diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt index 0b1d2e5e05a..26418e4dcef 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt @@ -84,6 +84,7 @@ import io.getstream.chat.android.compose.ui.channels.list.ChannelItem import io.getstream.chat.android.compose.ui.channels.list.ChannelList import io.getstream.chat.android.compose.ui.components.SearchInput import io.getstream.chat.android.compose.ui.components.channels.ChannelOptionItemVisibility +import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelActions import io.getstream.chat.android.compose.ui.mentions.MentionList import io.getstream.chat.android.compose.ui.theme.ChannelOptionsTheme import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -377,6 +378,13 @@ class ChannelsActivity : ComponentActivity() { val selectedChannel = delegatedSelectedChannel if (selectedChannel != null) { + val channelActions = buildDefaultChannelActions( + selectedChannel = selectedChannel, + isMuted = channelsViewModel.isChannelMuted(selectedChannel.cid), + ownCapabilities = selectedChannel.ownCapabilities, + viewModel = channelsViewModel, + onViewInfoAction = ::viewChannelInfo, + ) SelectedChannelMenu( modifier = Modifier .padding(16.dp) @@ -384,10 +392,10 @@ class ChannelsActivity : ComponentActivity() { .wrapContentHeight() .align(Alignment.Center), shape = RoundedCornerShape(16.dp), - isMuted = channelsViewModel.isChannelMuted(selectedChannel.cid), selectedChannel = selectedChannel, currentUser = user, - onChannelOptionClick = { action -> channelsViewModel.performChannelAction(action) }, + channelActions = channelActions, + onChannelOptionClick = { action -> channelsViewModel.executeOrConfirm(action) }, onDismiss = { channelsViewModel.dismissChannelAction() }, ) } diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt index 35db2cc526e..25aece61323 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/component/CustomChatComponentFactory.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.sample.ui.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -25,6 +26,8 @@ import io.getstream.chat.android.compose.sample.ui.location.LocationComponentFac import io.getstream.chat.android.compose.sample.vm.SharedLocationViewModelFactory import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.channels.list.ChannelItem +import io.getstream.chat.android.compose.ui.channels.list.LocalSwipeRevealCoordinator +import io.getstream.chat.android.compose.ui.channels.list.SwipeableChannelItem import io.getstream.chat.android.compose.ui.theme.ChatComponentFactory import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.models.Channel @@ -42,21 +45,40 @@ class CustomChatComponentFactory( onChannelClick: (Channel) -> Unit, onChannelLongClick: (Channel) -> Unit, ) { - ChannelItem( - modifier = Modifier - .animateItem() - .run { - // Highlight the item background color if it is pinned - if (channelItem.channel.isPinned()) { - background(color = ChatTheme.colors.backgroundCoreHighlight) - } else { - this - } - }, - channelItem = channelItem, - currentUser = currentUser, - onChannelClick = onChannelClick, - onChannelLongClick = onChannelLongClick, - ) + val coordinator = LocalSwipeRevealCoordinator.current + val swipeEnabled = ChatTheme.config.channelList.swipeActionsEnabled && coordinator != null + val itemModifier = Modifier + .animateItem() + .run { + if (channelItem.channel.isPinned()) { + background(color = ChatTheme.colors.backgroundCoreHighlight) + } else { + this + } + } + + if (swipeEnabled) { + SwipeableChannelItem( + channelCid = channelItem.channel.cid, + backgroundColor = ChatTheme.colors.backgroundCoreApp, + swipeActions = { Row { ChannelSwipeActions(channelItem) } }, + ) { + ChannelItem( + modifier = itemModifier, + channelItem = channelItem, + currentUser = currentUser, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + ) + } + } else { + ChannelItem( + modifier = itemModifier, + channelItem = channelItem, + currentUser = currentUser, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + ) + } } } diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 904d83a7c56..f16cf5346b6 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -50,16 +50,6 @@ public final class io/getstream/chat/android/compose/state/QueryConfig { public fun toString ()Ljava/lang/String; } -public final class io/getstream/chat/android/compose/state/channels/list/ChannelOptionState { - public static final field $stable I - public synthetic fun (Ljava/lang/String;JLandroidx/compose/ui/graphics/painter/Painter;JLio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAction ()Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction; - public final fun getIconColor-0d7_KjU ()J - public final fun getIconPainter ()Landroidx/compose/ui/graphics/painter/Painter; - public final fun getTitle ()Ljava/lang/String; - public final fun getTitleColor-0d7_KjU ()J -} - public final class io/getstream/chat/android/compose/state/channels/list/ChannelsState { public static final field $stable I public fun ()V @@ -89,15 +79,16 @@ public abstract class io/getstream/chat/android/compose/state/channels/list/Item public final class io/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState : io/getstream/chat/android/compose/state/channels/list/ItemState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;Z)V - public synthetic fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/Channel;ZZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;Z)V + public synthetic fun (Lio/getstream/chat/android/models/Channel;ZZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/Channel; public final fun component2 ()Z - public final fun component3 ()Ljava/util/List; - public final fun component4 ()Lio/getstream/chat/android/models/DraftMessage; - public final fun component5 ()Z - public final fun copy (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;Z)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ZILjava/lang/Object;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; + public final fun component3 ()Z + public final fun component4 ()Ljava/util/List; + public final fun component5 ()Lio/getstream/chat/android/models/DraftMessage; + public final fun component6 ()Z + public final fun copy (Lio/getstream/chat/android/models/Channel;ZZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;Z)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/Channel;ZZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ZILjava/lang/Object;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; public fun equals (Ljava/lang/Object;)Z public final fun getChannel ()Lio/getstream/chat/android/models/Channel; public final fun getDraftMessage ()Lio/getstream/chat/android/models/DraftMessage; @@ -105,6 +96,7 @@ public final class io/getstream/chat/android/compose/state/channels/list/ItemSta public final fun getTypingUsers ()Ljava/util/List; public fun hashCode ()I public final fun isMuted ()Z + public final fun isPinned ()Z public final fun isSelected ()Z public fun toString ()Ljava/lang/String; } @@ -1232,7 +1224,7 @@ public final class io/getstream/chat/android/compose/ui/channels/info/Composable } public final class io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenuKt { - public static final fun SelectedChannelMenu-pX9LQoI (Lio/getstream/chat/android/models/Channel;ZLio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Ljava/util/List;Landroidx/compose/ui/graphics/Shape;JLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun SelectedChannelMenu-Ic2awPA (Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;JLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/channels/list/ChannelItemKt { @@ -1309,10 +1301,53 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; } +public final class io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActionsKt { + public static final fun DefaultChannelSwipeActions (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Landroidx/compose/runtime/Composer;I)V +} + public final class io/getstream/chat/android/compose/ui/channels/list/SearchResultItemKt { public static final fun SearchResultItem (Lio/getstream/chat/android/compose/state/channels/list/ItemState$SearchResultItemState;Lio/getstream/chat/android/models/User;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeActionItemKt { + public static final fun SwipeActionItem (Landroidx/compose/ui/graphics/painter/Painter;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun SwipeActionItem-jB83MbM (Landroidx/compose/ui/graphics/painter/Painter;Ljava/lang/String;Lkotlin/jvm/functions/Function0;JJLandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle : java/lang/Enum { + public static final field Destructive Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle; + public static final field Primary Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle; + public static final field Secondary Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle; + public static fun values ()[Lio/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle; +} + +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator { + public static final field $stable I + public fun ()V + public final fun closeAll (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun onItemOpened (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun register (Ljava/lang/String;Landroidx/compose/foundation/gestures/AnchoredDraggableState;)V + public final fun unregister (Ljava/lang/String;)V +} + +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinatorKt { + public static final fun getLocalSwipeRevealCoordinator ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue : java/lang/Enum { + public static final field Closed Lio/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue; + public static final field Open Lio/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue; + public static fun values ()[Lio/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue; +} + +public final class io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItemKt { + public static final fun SwipeableChannelItem-fWhpE4E (Ljava/lang/String;Landroidx/compose/ui/Modifier;ZJLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/chats/ChatListContentMode : java/lang/Enum { public static final field Channels Lio/getstream/chat/android/compose/ui/chats/ChatListContentMode; public static final field Mentions Lio/getstream/chat/android/compose/ui/chats/ChatListContentMode; @@ -1564,7 +1599,7 @@ public final class io/getstream/chat/android/compose/ui/components/channels/Chan public final class io/getstream/chat/android/compose/ui/components/channels/ChannelOptionsKt { public static final fun ChannelOptions (Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V - public static final fun buildDefaultChannelOptionsState (Lio/getstream/chat/android/models/Channel;ZLjava/util/Set;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun buildDefaultChannelActions (Lio/getstream/chat/android/models/Channel;ZLjava/util/Set;Lio/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Ljava/util/List; } public final class io/getstream/chat/android/compose/ui/components/channels/ComposableSingletons$ChannelMembersItemKt { @@ -2752,13 +2787,15 @@ public final class io/getstream/chat/android/compose/ui/pinned/PinnedMessageList public final class io/getstream/chat/android/compose/ui/theme/ChannelListConfig { public static final field $stable I public fun ()V - public fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;)V - public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;Z)V + public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; - public final fun copy (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public final fun component2 ()Z + public final fun copy (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;Z)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ZILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; public fun equals (Ljava/lang/Object;)Z public final fun getMuteIndicatorPosition ()Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public final fun getSwipeActionsEnabled ()Z public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2854,12 +2891,13 @@ public abstract interface class io/getstream/chat/android/compose/ui/theme/ChatC public abstract fun ChannelMediaAttachmentsPreviewTopBar (Lio/getstream/chat/android/ui/common/state/channel/attachments/ChannelAttachmentsViewState$Content$Item;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelMediaAttachmentsPreviewTopBarTitle (Lio/getstream/chat/android/ui/common/state/channel/attachments/ChannelAttachmentsViewState$Content$Item;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelMediaAttachmentsTopBar (Landroidx/compose/foundation/lazy/grid/LazyGridState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public abstract fun ChannelMenu (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;ZLio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public abstract fun ChannelMenu (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelMenuCenterContent (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelMenuHeaderContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelMenuOptions (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;I)V - public abstract fun ChannelOptionsItem (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/compose/state/channels/list/ChannelOptionState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public abstract fun ChannelOptionsItemLeadingIcon (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/compose/state/channels/list/ChannelOptionState;Landroidx/compose/runtime/Composer;I)V + public abstract fun ChannelOptionsItem (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public abstract fun ChannelOptionsItemLeadingIcon (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;Landroidx/compose/runtime/Composer;I)V + public abstract fun ChannelSwipeActions (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Landroidx/compose/runtime/Composer;I)V public abstract fun CustomAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V public abstract fun DirectChannelInfoAvatarContainer (Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public abstract fun DirectChannelInfoTopBar (Lio/getstream/chat/android/ui/common/state/messages/list/ChannelHeaderViewState;Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V @@ -3049,12 +3087,13 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public static fun ChannelMediaAttachmentsPreviewTopBar (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/ui/common/state/channel/attachments/ChannelAttachmentsViewState$Content$Item;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public static fun ChannelMediaAttachmentsPreviewTopBarTitle (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/ui/common/state/channel/attachments/ChannelAttachmentsViewState$Content$Item;Landroidx/compose/runtime/Composer;I)V public static fun ChannelMediaAttachmentsTopBar (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/lazy/grid/LazyGridState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public static fun ChannelMenu (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;ZLio/getstream/chat/android/models/User;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public static fun ChannelMenu (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public static fun ChannelMenuCenterContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;I)V public static fun ChannelMenuHeaderContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public static fun ChannelMenuOptions (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Ljava/util/List;Landroidx/compose/runtime/Composer;I)V - public static fun ChannelOptionsItem (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/compose/state/channels/list/ChannelOptionState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V - public static fun ChannelOptionsItemLeadingIcon (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/compose/state/channels/list/ChannelOptionState;Landroidx/compose/runtime/Composer;I)V + public static fun ChannelOptionsItem (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public static fun ChannelOptionsItemLeadingIcon (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;Landroidx/compose/runtime/Composer;I)V + public static fun ChannelSwipeActions (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Landroidx/compose/runtime/Composer;I)V public static fun CustomAttachmentContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V public static fun DirectChannelInfoAvatarContainer (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public static fun DirectChannelInfoTopBar (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Lio/getstream/chat/android/ui/common/state/messages/list/ChannelHeaderViewState;Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V @@ -4212,8 +4251,10 @@ public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelL public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;ILjava/lang/Integer;Ljava/lang/Integer;Lio/getstream/chat/android/client/api/event/ChatEventHandlerFactory;JZLkotlinx/coroutines/flow/Flow;)V public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;ILjava/lang/Integer;Ljava/lang/Integer;Lio/getstream/chat/android/client/api/event/ChatEventHandlerFactory;JZLkotlinx/coroutines/flow/Flow;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun archiveChannel (Lio/getstream/chat/android/models/Channel;)V + public final fun confirmPendingAction ()V public final fun deleteConversation (Lio/getstream/chat/android/models/Channel;)V public final fun dismissChannelAction ()V + public final fun executeOrConfirm (Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;)V public final fun getActiveChannelAction ()Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction; public final fun getChannelMutes ()Lkotlinx/coroutines/flow/StateFlow; public final fun getChannelsState ()Lio/getstream/chat/android/compose/state/channels/list/ChannelsState; @@ -4226,7 +4267,6 @@ public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelL public final fun leaveGroup (Lio/getstream/chat/android/models/Channel;)V public final fun loadMore ()V public final fun muteChannel (Lio/getstream/chat/android/models/Channel;)V - public final fun performChannelAction (Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;)V public final fun pinChannel (Lio/getstream/chat/android/models/Channel;)V public final fun refresh ()V public final fun selectChannel (Lio/getstream/chat/android/models/Channel;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ChannelOptionState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ChannelOptionState.kt deleted file mode 100644 index dc0865b8134..00000000000 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ChannelOptionState.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.chat.android.compose.state.channels.list - -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction - -/** - * UI representation of a Channel option, when the user selects a channel in the list. - * - * @param title The title to represent the action. - * @param titleColor The color of the title text. - * @param iconPainter The icon to represent the action. - * @param iconColor The color of the icon. - * @param action The [ChannelAction] the option represents. - */ -public class ChannelOptionState( - public val title: String, - public val titleColor: Color, - public val iconPainter: Painter, - public val iconColor: Color, - public val action: ChannelAction, -) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt index f3006cd7335..f313aa401f2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt @@ -32,6 +32,7 @@ public sealed class ItemState { * * @param channel The channel to show. * @param isMuted If the channel is muted for the current user. + * @param isPinned If the channel is pinned for the current user. * @param typingUsers The list of users currently typing in the channel. * @param draftMessage The draft message for the current user in the channel. * @param isSelected Whether this channel is currently selected (e.g. via long-press context menu). @@ -39,6 +40,7 @@ public sealed class ItemState { public data class ChannelItemState( val channel: Channel, val isMuted: Boolean = false, + val isPinned: Boolean = false, val typingUsers: List = emptyList(), val draftMessage: DraftMessage? = null, val isSelected: Boolean = false, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt index bedbd2022f0..dc5d2e38ab4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt @@ -41,27 +41,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.channels.list.SearchQuery import io.getstream.chat.android.compose.ui.channels.list.ChannelList import io.getstream.chat.android.compose.ui.components.SimpleDialog +import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelActions import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channels.ChannelViewModelFactory import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Message -import io.getstream.chat.android.ui.common.state.channels.actions.ArchiveChannel -import io.getstream.chat.android.ui.common.state.channels.actions.DeleteConversation -import io.getstream.chat.android.ui.common.state.channels.actions.LeaveGroup -import io.getstream.chat.android.ui.common.state.channels.actions.MuteChannel -import io.getstream.chat.android.ui.common.state.channels.actions.PinChannel -import io.getstream.chat.android.ui.common.state.channels.actions.UnarchiveChannel -import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteChannel -import io.getstream.chat.android.ui.common.state.channels.actions.UnpinChannel import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo /** @@ -191,6 +182,17 @@ public fun ChannelsScreen( enter = fadeIn(), exit = fadeOut(animationSpec = tween(durationMillis = AnimationConstants.DefaultDurationMillis / 2)), ) { + val channelActions = buildDefaultChannelActions( + selectedChannel = channel, + isMuted = listViewModel.isChannelMuted(channel.cid), + ownCapabilities = channel.ownCapabilities, + viewModel = listViewModel, + onViewInfoAction = { ch -> + listViewModel.dismissChannelAction() + onViewChannelInfoAction(ch) + }, + ) + ChatTheme.componentFactory.ChannelMenu( modifier = Modifier .align(Alignment.BottomCenter) @@ -206,23 +208,11 @@ public fun ChannelsScreen( ), selectedChannel = channel, currentUser = user, - isMuted = listViewModel.isChannelMuted(channel.cid), + channelActions = channelActions, onChannelOptionClick = remember(listViewModel) { { action -> - when (action) { - is ViewInfo -> { - listViewModel.dismissChannelAction() - onViewChannelInfoAction(action.channel) - } - is MuteChannel -> listViewModel.muteChannel(action.channel) - is UnmuteChannel -> listViewModel.unmuteChannel(action.channel) - is PinChannel -> listViewModel.pinChannel(action.channel) - is UnpinChannel -> listViewModel.unpinChannel(action.channel) - is ArchiveChannel -> listViewModel.archiveChannel(action.channel) - is UnarchiveChannel -> listViewModel.unarchiveChannel(action.channel) - else -> listViewModel.performChannelAction(action) - } + listViewModel.executeOrConfirm(action) } }, onDismiss = remember(listViewModel) { { listViewModel.dismissChannelAction() } }, @@ -230,32 +220,14 @@ public fun ChannelsScreen( } val activeAction = listViewModel.activeChannelAction + val popup = activeAction?.confirmationPopup - if (activeAction is LeaveGroup) { - SimpleDialog( - modifier = Modifier.padding(16.dp), - title = stringResource( - id = R.string.stream_compose_selected_channel_menu_leave_group_confirmation_title, - ), - message = stringResource( - id = R.string.stream_compose_selected_channel_menu_leave_group_confirmation_message, - ChatTheme.channelNameFormatter.formatChannelName(activeAction.channel, user), - ), - onPositiveAction = remember(listViewModel) { { listViewModel.leaveGroup(activeAction.channel) } }, - onDismiss = remember(listViewModel) { { listViewModel.dismissChannelAction() } }, - ) - } else if (activeAction is DeleteConversation) { + if (popup != null) { SimpleDialog( modifier = Modifier.padding(16.dp), - title = stringResource( - id = R.string.stream_compose_selected_channel_menu_delete_conversation_confirmation_title, - ), - message = stringResource( - id = R.string.stream_compose_selected_channel_menu_delete_conversation_confirmation_message, - ChatTheme.channelNameFormatter.formatChannelName(activeAction.channel, user), - ), - onPositiveAction = - remember(listViewModel) { { listViewModel.deleteConversation(activeAction.channel) } }, + title = popup.title, + message = popup.message, + onPositiveAction = remember(listViewModel) { { listViewModel.confirmPendingAction() } }, onDismiss = remember(listViewModel) { { listViewModel.dismissChannelAction() } }, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt index ef50a0a5022..78373eb39ce 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/info/SelectedChannelMenu.kt @@ -17,10 +17,13 @@ package io.getstream.chat.android.compose.ui.channels.info import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -30,34 +33,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.getstream.chat.android.compose.state.channels.list.ChannelOptionState import io.getstream.chat.android.compose.ui.components.SimpleMenu -import io.getstream.chat.android.compose.ui.components.channels.ChannelMembers -import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelOptionsState +import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.ui.util.getMembersStatusText -import io.getstream.chat.android.compose.ui.util.isOneToOne import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.User import io.getstream.chat.android.previewdata.PreviewChannelData import io.getstream.chat.android.previewdata.PreviewUserData import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction +import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo /** * Shows special UI when an item is selected. * It also prepares the available options for the channel, based on if we're an admin or not. * * @param selectedChannel The channel the user selected. - * @param isMuted If the channel is muted for the current user. + * @param currentUser The currently logged-in user data. + * @param channelActions The list of actions to show in the menu. * @param onChannelOptionClick Handler for when the user selects a channel option. * @param onDismiss Handler called when the dialog is dismissed. * @param modifier Modifier for styling. - * @param channelOptions The list of options to show in the UI, according to user permissions. * @param shape The shape of the component. * @param overlayColor The color applied to the overlay. * @param headerContent The content shown at the top of the dialog. @@ -66,16 +66,11 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction @Composable public fun SelectedChannelMenu( selectedChannel: Channel, - isMuted: Boolean, currentUser: User?, + channelActions: List, onChannelOptionClick: (ChannelAction) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - channelOptions: List = buildDefaultChannelOptionsState( - selectedChannel = selectedChannel, - isMuted = isMuted, - ownCapabilities = selectedChannel.ownCapabilities, - ), shape: Shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), overlayColor: Color = ChatTheme.colors.backgroundCoreScrim, headerContent: @Composable ColumnScope.() -> Unit = { @@ -92,7 +87,7 @@ public fun SelectedChannelMenu( ChannelMenuCenterContent( modifier = Modifier, onChannelOptionClick = onChannelOptionClick, - channelOptions = channelOptions, + channelActions = channelActions, ) } }, @@ -118,44 +113,52 @@ internal fun DefaultSelectedChannelMenuHeaderContent( selectedChannel: Channel, currentUser: User?, ) { - val channelMembers = selectedChannel.members - val membersToDisplay = if (selectedChannel.isOneToOne(currentUser)) { - channelMembers.filter { it.user.id != currentUser?.id } - } else { - channelMembers - } - - Text( + Row( modifier = Modifier .fillMaxWidth() .padding( - start = StreamTokens.spacingMd, // 16dp (was 16.dp, now token-aligned) - end = StreamTokens.spacingMd, // 16dp (was 16.dp) - top = StreamTokens.spacingMd, // 16dp (was 16.dp) + start = StreamTokens.spacingMd, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingSm, ), - textAlign = TextAlign.Center, - text = ChatTheme.channelNameFormatter.formatChannelName(selectedChannel, currentUser), - style = ChatTheme.typography.headingSmall, - color = ChatTheme.colors.textPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = selectedChannel.getMembersStatusText( - context = LocalContext.current, + verticalAlignment = Alignment.CenterVertically, + ) { + ChatTheme.componentFactory.ChannelAvatar( + modifier = Modifier.size(AvatarSize.ExtraLarge), + channel = selectedChannel, currentUser = currentUser, - userPresence = ChatTheme.userPresence, - ), - style = ChatTheme.typography.captionDefault, - color = ChatTheme.colors.textSecondary, - ) + showIndicator = true, + showBorder = false, + ) - ChannelMembers( - members = membersToDisplay, - currentUser = currentUser, - ) + Column( + modifier = Modifier + .padding(start = StreamTokens.spacingSm) + .weight(1f), + ) { + Text( + text = ChatTheme.channelNameFormatter.formatChannelName( + selectedChannel, + currentUser, + ), + style = ChatTheme.typography.headingSmall, + color = ChatTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = selectedChannel.getMembersStatusText( + context = LocalContext.current, + currentUser = currentUser, + userPresence = ChatTheme.userPresence, + ), + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, + maxLines = 1, + ) + } + } } /** @@ -168,6 +171,7 @@ internal fun DefaultSelectedChannelMenuHeaderContent( private fun SelectedChannelMenuCenteredDialogPreview() { ChatTheme { Box(modifier = Modifier.fillMaxSize()) { + val channel = PreviewChannelData.channelWithManyMembers SelectedChannelMenu( modifier = Modifier .padding(16.dp) @@ -175,9 +179,11 @@ private fun SelectedChannelMenuCenteredDialogPreview() { .wrapContentHeight() .align(Alignment.Center), shape = RoundedCornerShape(16.dp), - selectedChannel = PreviewChannelData.channelWithManyMembers, - isMuted = false, + selectedChannel = channel, currentUser = PreviewUserData.user1, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), onChannelOptionClick = {}, onDismiss = {}, ) @@ -195,15 +201,18 @@ private fun SelectedChannelMenuCenteredDialogPreview() { private fun SelectedChannelMenuBottomSheetDialogPreview() { ChatTheme { Box(modifier = Modifier.fillMaxSize()) { + val channel = PreviewChannelData.channelWithManyMembers SelectedChannelMenu( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .align(Alignment.BottomCenter), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - selectedChannel = PreviewChannelData.channelWithManyMembers, - isMuted = false, + selectedChannel = channel, currentUser = PreviewUserData.user1, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), onChannelOptionClick = {}, onDismiss = {}, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 7371a21b418..cfee61943f5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -251,7 +251,7 @@ internal fun RowScope.DefaultChannelItemCenterContent( .testTag("Stream_ChannelName") .weight(1f, fill = false), text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), - style = ChatTheme.typography.bodyDefault, + style = ChatTheme.typography.headingSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, color = ChatTheme.colors.textPrimary, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt index 175ba2c64a6..d8b9624f23b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt @@ -23,9 +23,11 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -50,6 +52,14 @@ import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.previewdata.PreviewChannelData import io.getstream.chat.android.previewdata.PreviewMessageData import io.getstream.chat.android.previewdata.PreviewUserData +import io.getstream.chat.android.ui.common.state.channels.actions.ArchiveChannel +import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction +import io.getstream.chat.android.ui.common.state.channels.actions.MuteChannel +import io.getstream.chat.android.ui.common.state.channels.actions.PinChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnarchiveChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnpinChannel +import kotlinx.coroutines.launch /** * Default ChannelList component, that relies on the [ChannelListViewModel] to load the data and @@ -150,25 +160,55 @@ public fun ChannelList( }, ) { val user by viewModel.user.collectAsState() + val scope = rememberCoroutineScope() + val swipeCoordinator = remember { SwipeRevealCoordinator() } + val swipeActionHandler: (ChannelAction) -> Unit = remember(viewModel) { + { + action -> + scope.launch { swipeCoordinator.closeAll() } + when (action) { + is MuteChannel -> viewModel.muteChannel(action.channel) + is UnmuteChannel -> viewModel.unmuteChannel(action.channel) + is PinChannel -> viewModel.pinChannel(action.channel) + is UnpinChannel -> viewModel.unpinChannel(action.channel) + is ArchiveChannel -> viewModel.archiveChannel(action.channel) + is UnarchiveChannel -> viewModel.unarchiveChannel(action.channel) + else -> viewModel.executeOrConfirm(action) + } + } + } + val moreClickHandler: (Channel) -> Unit = remember(viewModel) { + { + channel -> + scope.launch { swipeCoordinator.closeAll() } + viewModel.selectChannel(channel) + } + } - ChannelList( - modifier = modifier, - contentPadding = contentPadding, - channelsState = viewModel.channelsState, - currentUser = user, - lazyListState = lazyListState, - onLastItemReached = onLastItemReached, - onChannelClick = onChannelClick, - onChannelLongClick = onChannelLongClick, - loadingContent = loadingContent, - emptyContent = emptyContent, - emptySearchContent = emptySearchContent, - helperContent = helperContent, - loadingMoreContent = loadingMoreContent, - channelContent = channelContent, - searchResultContent = searchResultContent, - divider = divider, - ) + CompositionLocalProvider( + LocalSwipeRevealCoordinator provides swipeCoordinator, + LocalSwipeActionHandler provides swipeActionHandler, + LocalChannelMoreClickHandler provides moreClickHandler, + ) { + ChannelList( + modifier = modifier, + contentPadding = contentPadding, + channelsState = viewModel.channelsState, + currentUser = user, + lazyListState = lazyListState, + onLastItemReached = onLastItemReached, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + loadingContent = loadingContent, + emptyContent = emptyContent, + emptySearchContent = emptySearchContent, + helperContent = helperContent, + loadingMoreContent = loadingMoreContent, + channelContent = channelContent, + searchResultContent = searchResultContent, + divider = divider, + ) + } } /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt new file mode 100644 index 00000000000..cd4f409a337 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import io.getstream.chat.android.client.extensions.isArchive +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.state.channels.list.ItemState +import io.getstream.chat.android.compose.ui.util.isDistinct +import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.ui.common.state.channels.actions.ArchiveChannel +import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction +import io.getstream.chat.android.ui.common.state.channels.actions.MuteChannel +import io.getstream.chat.android.ui.common.state.channels.actions.PinChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnarchiveChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteChannel +import io.getstream.chat.android.ui.common.state.channels.actions.UnpinChannel +import kotlinx.coroutines.launch + +/** + * Default swipe actions for a channel list item. + * + * Shows two actions: + * - **More** (gray, left): Opens the channel options bottom sheet. + * - **Primary action** (blue, right): Archive for DMs, Mute for groups — with fallback priority. + * + * The primary action is resolved via a priority list: + * - DM: Archive → Mute → Pin + * - Group: Mute → Archive → Pin + * + * Each action is a self-executing [ChannelAction] that invokes its handler via + * [LocalSwipeActionHandler]. + * + * @param channelItem The channel item state to build actions for. + */ +@Composable +public fun RowScope.DefaultChannelSwipeActions(channelItem: ItemState.ChannelItemState) { + val handler = LocalSwipeActionHandler.current ?: return + val coordinator = LocalSwipeRevealCoordinator.current + val moreHandler = LocalChannelMoreClickHandler.current + val scope = rememberCoroutineScope() + val channel = channelItem.channel + + fun onAction(action: ChannelAction) { + handler(action) + scope.launch { coordinator?.closeAll() } + } + + // "More" action — opens the channel options sheet + if (moreHandler != null) { + SwipeActionItem( + icon = painterResource(R.drawable.stream_compose_ic_more_options), + label = stringResource(R.string.stream_compose_swipe_action_more), + onClick = { + scope.launch { coordinator?.closeAll() } + moreHandler(channel) + }, + style = SwipeActionStyle.Secondary, + ) + } + + // Primary action (rightmost) — resolved by channel type and capabilities + val primaryAction = resolvePrimarySwipeAction( + channel = channel, + isMuted = channelItem.isMuted, + isPinned = channelItem.isPinned, + ) + if (primaryAction != null) { + SwipeActionItem( + icon = painterResource(primaryAction.icon), + label = primaryAction.label, + onClick = { onAction(primaryAction) }, + style = SwipeActionStyle.Primary, + ) + } +} + +/** + * Resolves the primary swipe action based on channel type and capabilities. + * + * DM priority: Archive → Mute → Pin. + * Group priority: Mute → Archive → Pin. + * + * Archive and Pin are always available (membership operations, no capability gate). + * Mute requires [ChannelCapabilities.MUTE_CHANNEL]. + */ +@Composable +private fun resolvePrimarySwipeAction( + channel: Channel, + isMuted: Boolean, + isPinned: Boolean, +): ChannelAction? { + val capabilities = channel.ownCapabilities + val isArchived = channel.isArchive() + val canMute = capabilities.contains(ChannelCapabilities.MUTE_CHANNEL) + val isDM = channel.isDistinct() && channel.members.size == 2 + + // Build candidate actions with resolved labels + val archiveAction: ChannelAction = if (isArchived) { + UnarchiveChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_unarchive), + onAction = {}, + ) + } else { + ArchiveChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_archive), + onAction = {}, + ) + } + + val muteAction: ChannelAction? = if (canMute) { + if (isMuted) { + UnmuteChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_unmute), + onAction = {}, + ) + } else { + MuteChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_mute), + onAction = {}, + ) + } + } else { + null + } + + val pinAction: ChannelAction = if (isPinned) { + UnpinChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_unpin), + onAction = {}, + ) + } else { + PinChannel( + channel = channel, + label = stringResource(R.string.stream_compose_swipe_action_pin), + onAction = {}, + ) + } + + val candidates: List = if (isDM) { + listOf(archiveAction, muteAction, pinAction) + } else { + listOf(muteAction, archiveAction, pinAction) + } + + return candidates.firstOrNull { it != null } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionItem.kt new file mode 100644 index 00000000000..95b0264a5e2 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionItem.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +/** + * A single swipe action button displayed behind a channel list item. + * + * @param icon The icon to display. + * @param label The label text below the icon. + * @param onClick Called when this action is tapped. + * @param backgroundColor The background color of this action. + * @param contentColor The color for the icon and label. + * @param modifier Modifier for styling. + */ +@Composable +public fun SwipeActionItem( + icon: Painter, + label: String, + onClick: () -> Unit, + backgroundColor: Color, + contentColor: Color, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .width(80.dp) + .fillMaxHeight() + .background(backgroundColor) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = icon, + contentDescription = label, + tint = contentColor, + modifier = Modifier.size(24.dp), + ) + } +} + +/** + * A single swipe action button using [SwipeActionStyle] for slot-based coloring. + * + * @param icon The icon to display. + * @param label The label text below the icon. + * @param onClick Called when this action is tapped. + * @param style The visual style determining background and content colors. + * @param modifier Modifier for styling. + */ +@Composable +public fun SwipeActionItem( + icon: Painter, + label: String, + onClick: () -> Unit, + style: SwipeActionStyle, + modifier: Modifier = Modifier, +) { + val colors = ChatTheme.colors + val (bg, content) = when (style) { + SwipeActionStyle.Primary -> colors.accentPrimary to Color.White + SwipeActionStyle.Secondary -> colors.backgroundCoreSurface to colors.textPrimary + SwipeActionStyle.Destructive -> colors.accentError to Color.White + } + SwipeActionItem( + icon = icon, + label = label, + onClick = onClick, + backgroundColor = bg, + contentColor = content, + modifier = modifier, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt new file mode 100644 index 00000000000..fa2a5184c01 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeActionStyle.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +/** + * Visual style for a swipe action slot. + * + * The style determines the background and content colors of the action button. + * It is slot-based: the same action can be styled differently depending on its position. + */ +public enum class SwipeActionStyle { + /** Blue background ([ChatTheme.colors.accentPrimary]), white content. */ + Primary, + + /** Gray background ([ChatTheme.colors.accentNeutral]), white content. */ + Secondary, + + /** Red background ([ChatTheme.colors.accentError]), white content. */ + Destructive, +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt new file mode 100644 index 00000000000..95ec4e23521 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealCoordinator.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.animateTo +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction + +/** + * Coordinates swipe-to-reveal state across multiple channel list items, + * ensuring only one item is open at a time. + */ +public class SwipeRevealCoordinator { + + private val registry = mutableMapOf>() + + /** + * Registers a swipe state for the given channel CID. + * Called from [SwipeableChannelItem]'s DisposableEffect. + */ + public fun register(cid: String, state: AnchoredDraggableState) { + registry[cid] = state + } + + /** + * Unregisters the swipe state for the given channel CID. + * Called when [SwipeableChannelItem] leaves composition. + */ + public fun unregister(cid: String) { + registry.remove(cid) + } + + /** + * Notifies the coordinator that an item has been opened. + * All other open items will be animated to [SwipeRevealValue.Closed]. + */ + public suspend fun onItemOpened(cid: String) { + registry.forEach { (key, state) -> + if (key != cid && state.currentValue == SwipeRevealValue.Open) { + state.animateTo(SwipeRevealValue.Closed) + } + } + } + + /** + * Closes all currently open items. + */ + public suspend fun closeAll() { + registry.forEach { (_, state) -> + if (state.currentValue == SwipeRevealValue.Open) { + state.animateTo(SwipeRevealValue.Closed) + } + } + } +} + +/** + * Provides the [SwipeRevealCoordinator] to child composables. + * `null` means swipe actions are not available in the current composition tree. + */ +public val LocalSwipeRevealCoordinator: ProvidableCompositionLocal = + compositionLocalOf { null } + +/** + * Provides a handler for swipe channel actions (pin, mute, archive, etc.). + * `null` means no handler is available. + */ +internal val LocalSwipeActionHandler = compositionLocalOf<((ChannelAction) -> Unit)?> { null } + +/** + * Provides a handler for the "More" swipe action that opens the channel options sheet. + * Maps to the same flow as long-press: selecting the channel to show [SelectedChannelMenu]. + * `null` means no handler is available. + */ +internal val LocalChannelMoreClickHandler = compositionLocalOf<((Channel) -> Unit)?> { null } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue.kt new file mode 100644 index 00000000000..3cd4bcfeca2 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeRevealValue.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +/** + * Represents the state of a swipe-to-reveal action on a channel list item. + */ +public enum class SwipeRevealValue { + /** The swipe actions are hidden. */ + Closed, + + /** The swipe actions are fully revealed. */ + Open, +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt new file mode 100644 index 00000000000..f682d0dcff2 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.channels.list + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.coroutines.flow.filter +import kotlin.math.roundToInt + +/** + * A wrapper that adds swipe-to-reveal actions behind channel list item content. + * + * Uses [AnchoredDraggableState] to provide smooth swipe interaction with snap-to anchors. + * Supports RTL layouts and coordinates with [SwipeRevealCoordinator] for single-open behavior. + * + * @param channelCid The channel CID used for coordinator registration. + * @param modifier Modifier for styling. + * @param enabled Whether swiping is enabled. + * @param backgroundColor Opaque background for the foreground layer, hiding actions when closed. + * @param swipeActions Composable content for the action buttons revealed by swiping. + * @param content The channel item content displayed on top. + */ +@Composable +public fun SwipeableChannelItem( + channelCid: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + backgroundColor: Color = Color.Unspecified, + swipeActions: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val coordinator = LocalSwipeRevealCoordinator.current + + val anchoredDraggableState = remember { + AnchoredDraggableState(initialValue = SwipeRevealValue.Closed) + } + + var actionsWidthPx by remember { mutableIntStateOf(0) } + + // LaunchedEffect (not SideEffect) so the key triggers recomposition after onSizeChanged. + LaunchedEffect(actionsWidthPx) { + if (actionsWidthPx > 0) { + val newAnchors = DraggableAnchors { + SwipeRevealValue.Closed at 0f + SwipeRevealValue.Open at -actionsWidthPx.toFloat() + } + anchoredDraggableState.updateAnchors(newAnchors, anchoredDraggableState.currentValue) + } + } + + // Register/unregister with coordinator + DisposableEffect(channelCid, coordinator) { + coordinator?.register(channelCid, anchoredDraggableState) + onDispose { + coordinator?.unregister(channelCid) + } + } + + // Notify coordinator when this item opens + LaunchedEffect(anchoredDraggableState) { + snapshotFlow { anchoredDraggableState.currentValue } + .filter { it == SwipeRevealValue.Open } + .collect { coordinator?.onItemOpened(channelCid) } + } + + Box( + modifier = modifier + .height(IntrinsicSize.Min) + .clipToBounds(), + ) { + // Background: action buttons + Row( + modifier = Modifier + .fillMaxHeight() + .align(if (isRtl) Alignment.CenterStart else Alignment.CenterEnd) + .onSizeChanged { size -> actionsWidthPx = size.width }, + ) { + swipeActions() + } + + // Foreground: channel item content (opaque bg hides actions when closed) + Box( + modifier = Modifier + .offset { + val raw = anchoredDraggableState.offset + val x = if (raw.isNaN()) 0 else raw.roundToInt() + IntOffset(x = if (isRtl) -x else x, y = 0) + } + .background(backgroundColor) + .anchoredDraggable( + state = anchoredDraggableState, + orientation = Orientation.Horizontal, + enabled = enabled, + ), + ) { + content() + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt index afd1f75a5b5..6823f4bd2a7 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt @@ -16,28 +16,27 @@ package io.getstream.chat.android.compose.ui.components.channels +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import io.getstream.chat.android.client.extensions.isArchive import io.getstream.chat.android.client.extensions.isPinned import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.channels.list.ChannelOptionState -import io.getstream.chat.android.compose.ui.components.StreamHorizontalDivider import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.util.extensions.toSet +import io.getstream.chat.android.compose.ui.theme.StreamTokens +import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.previewdata.PreviewChannelData import io.getstream.chat.android.ui.common.state.channels.actions.ArchiveChannel -import io.getstream.chat.android.ui.common.state.channels.actions.Cancel import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction +import io.getstream.chat.android.ui.common.state.channels.actions.ConfirmationPopup import io.getstream.chat.android.ui.common.state.channels.actions.DeleteConversation import io.getstream.chat.android.ui.common.state.channels.actions.LeaveGroup import io.getstream.chat.android.ui.common.state.channels.actions.MuteChannel @@ -52,13 +51,13 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo * * It sets up different actions that we provide, based on user permissions. * - * @param options The list of options to show in the UI, according to user permissions. + * @param actions The list of channel actions to show in the UI. * @param onChannelOptionClick Handler for when the user selects a channel action. * @param modifier Modifier for styling. */ @Composable public fun ChannelOptions( - options: List, + actions: List, onChannelOptionClick: (ChannelAction) -> Unit, modifier: Modifier = Modifier, ) { @@ -66,15 +65,14 @@ public fun ChannelOptions( modifier = modifier .fillMaxWidth() .wrapContentHeight(), + contentPadding = PaddingValues(bottom = StreamTokens.spacingXs), ) { - items(options) { option -> - StreamHorizontalDivider() - + items(actions) { action -> with(ChatTheme.componentFactory) { ChannelOptionsItem( modifier = Modifier, - option = option, - onClick = { onChannelOptionClick(option.action) }, + action = action, + onClick = { onChannelOptionClick(action) }, ) } } @@ -82,177 +80,177 @@ public fun ChannelOptions( } /** - * Builds the default list of channel options, based on the current user and the state of the channel. + * Builds the default list of channel actions, based on the current user permissions and channel state. + * Each action is self-describing and carries its icon, label, and execution handler. * * @param selectedChannel The currently selected channel. * @param isMuted If the channel is muted or not. * @param ownCapabilities Set of capabilities the user is given for the current channel. - * @return The list of channel option items to display. + * @param viewModel The [ChannelListViewModel] to bind action handlers to. + * @param onViewInfoAction Handler invoked when the user selects the "View Info" action. + * @return The list of channel actions to display. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") @Composable -public fun buildDefaultChannelOptionsState( +public fun buildDefaultChannelActions( selectedChannel: Channel, isMuted: Boolean, ownCapabilities: Set, -): List { + viewModel: ChannelListViewModel, + onViewInfoAction: (Channel) -> Unit, +): List { val canLeaveChannel = ownCapabilities.contains(ChannelCapabilities.LEAVE_CHANNEL) val canDeleteChannel = ownCapabilities.contains(ChannelCapabilities.DELETE_CHANNEL) val canMuteChannel = ownCapabilities.contains(ChannelCapabilities.MUTE_CHANNEL) val optionVisibility = ChatTheme.channelOptionsTheme.optionVisibility + val channelName = ChatTheme.channelNameFormatter.formatChannelName( + selectedChannel, + viewModel.user.value, + ) return listOfNotNull( if (optionVisibility.isViewInfoVisible) { - ChannelOptionState( - title = stringResource(id = R.string.stream_compose_selected_channel_menu_view_info), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = R.drawable.stream_compose_ic_person), - iconColor = ChatTheme.colors.textSecondary, - action = ViewInfo(selectedChannel), + ViewInfo( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_view_info), + onAction = { onViewInfoAction(selectedChannel) }, ) } else { null }, - if (optionVisibility.isLeaveChannelVisible && canLeaveChannel) { - ChannelOptionState( - title = stringResource(id = R.string.stream_compose_selected_channel_menu_leave_group), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = R.drawable.stream_compose_ic_person_remove), - iconColor = ChatTheme.colors.textSecondary, - action = LeaveGroup(selectedChannel), - ) - } else { - null - }, - buildMuteOption( + buildMuteAction( canMuteChannel = optionVisibility.isMuteChannelVisible && canMuteChannel, isMuted = isMuted, selectedChannel = selectedChannel, + viewModel = viewModel, ), - buildPinOption( + buildPinAction( canPinChannel = optionVisibility.isPinChannelVisible, selectedChannel = selectedChannel, + viewModel = viewModel, ), - buildArchiveOption( + buildArchiveAction( canArchiveChannel = optionVisibility.isArchiveChannelVisible, selectedChannel = selectedChannel, + viewModel = viewModel, ), + if (optionVisibility.isLeaveChannelVisible && canLeaveChannel) { + LeaveGroup( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_leave_group), + onAction = { viewModel.leaveGroup(selectedChannel) }, + confirmationPopup = ConfirmationPopup( + title = stringResource( + id = R.string.stream_compose_selected_channel_menu_leave_group_confirmation_title, + ), + message = stringResource( + id = R.string.stream_compose_selected_channel_menu_leave_group_confirmation_message, + channelName, + ), + confirmButtonText = stringResource( + id = R.string.stream_compose_selected_channel_menu_leave_group, + ), + ), + ) + } else { + null + }, if (optionVisibility.isDeleteChannelVisible && canDeleteChannel) { - ChannelOptionState( - title = stringResource(id = R.string.stream_compose_selected_channel_menu_delete_conversation), - titleColor = ChatTheme.colors.accentError, - iconPainter = painterResource(id = R.drawable.stream_compose_ic_delete), - iconColor = ChatTheme.colors.accentError, - action = DeleteConversation(selectedChannel), + DeleteConversation( + channel = selectedChannel, + label = stringResource( + id = R.string.stream_compose_selected_channel_menu_delete_conversation, + ), + onAction = { viewModel.deleteConversation(selectedChannel) }, + confirmationPopup = ConfirmationPopup( + title = stringResource( + id = R.string + .stream_compose_selected_channel_menu_delete_conversation_confirmation_title, + ), + message = stringResource( + id = R.string + .stream_compose_selected_channel_menu_delete_conversation_confirmation_message, + channelName, + ), + confirmButtonText = stringResource( + id = R.string.stream_compose_selected_channel_menu_delete_conversation, + ), + ), ) } else { null }, - ChannelOptionState( - title = stringResource(id = R.string.stream_compose_selected_channel_menu_dismiss), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = R.drawable.stream_compose_ic_clear), - iconColor = ChatTheme.colors.textSecondary, - action = Cancel, - ), ) } /** - * Builds the pin option for the channel, based on the current state. - * - * @param canPinChannel If the user can pin the channel. - * @param selectedChannel The currently selected channel. + * Builds the pin action for the channel, based on the current state. */ @Composable -private fun buildPinOption( +private fun buildPinAction( canPinChannel: Boolean, selectedChannel: Channel, -) = when (selectedChannel.isPinned().takeIf { canPinChannel }) { - false -> Triple( - R.string.stream_compose_selected_channel_menu_pin_channel, - R.drawable.stream_compose_ic_pin, - PinChannel(selectedChannel), + viewModel: ChannelListViewModel, +): ChannelAction? = when (selectedChannel.isPinned().takeIf { canPinChannel }) { + false -> PinChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_pin_channel), + onAction = { viewModel.pinChannel(selectedChannel) }, ) - true -> Triple( - R.string.stream_compose_selected_channel_menu_unpin_channel, - R.drawable.stream_compose_ic_unpin, - UnpinChannel(selectedChannel), + true -> UnpinChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_unpin_channel), + onAction = { viewModel.unpinChannel(selectedChannel) }, ) null -> null -}?.let { - ChannelOptionState( - title = stringResource(id = it.first), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = it.second), - iconColor = ChatTheme.colors.textSecondary, - action = it.third, - ) } /** - * Builds the archive option for the channel, based on the current state. - * - * @param canArchiveChannel If the user can archive the channel. - * @param selectedChannel The currently selected channel. + * Builds the archive action for the channel, based on the current state. */ @Composable -private fun buildArchiveOption( +private fun buildArchiveAction( canArchiveChannel: Boolean, selectedChannel: Channel, -) = when (selectedChannel.isArchive().takeIf { canArchiveChannel }) { - false -> Triple( - R.string.stream_compose_selected_channel_menu_archive_channel, - R.drawable.stream_compose_ic_archive, - ArchiveChannel(selectedChannel), + viewModel: ChannelListViewModel, +): ChannelAction? = when (selectedChannel.isArchive().takeIf { canArchiveChannel }) { + false -> ArchiveChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_archive_channel), + onAction = { viewModel.archiveChannel(selectedChannel) }, ) - true -> Triple( - R.string.stream_compose_selected_channel_menu_unarchive_channel, - R.drawable.stream_compose_ic_unarchive, - UnarchiveChannel(selectedChannel), + true -> UnarchiveChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_unarchive_channel), + onAction = { viewModel.unarchiveChannel(selectedChannel) }, ) null -> null -}?.let { - ChannelOptionState( - title = stringResource(id = it.first), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = it.second), - iconColor = ChatTheme.colors.textSecondary, - action = it.third, - ) } @Composable -private fun buildMuteOption( +private fun buildMuteAction( canMuteChannel: Boolean, isMuted: Boolean, selectedChannel: Channel, -) = if (canMuteChannel) { - val uiData = when (isMuted) { - true -> Triple( - R.string.stream_compose_selected_channel_menu_unmute_channel, - R.drawable.stream_compose_ic_unmute, - UnmuteChannel(selectedChannel), + viewModel: ChannelListViewModel, +): ChannelAction? = if (canMuteChannel) { + when (isMuted) { + true -> UnmuteChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_unmute_channel), + onAction = { viewModel.unmuteChannel(selectedChannel) }, ) - false -> Triple( - R.string.stream_compose_selected_channel_menu_mute_channel, - R.drawable.stream_compose_ic_mute, - MuteChannel(selectedChannel), + false -> MuteChannel( + channel = selectedChannel, + label = stringResource(id = R.string.stream_compose_selected_channel_menu_mute_channel), + onAction = { viewModel.muteChannel(selectedChannel) }, ) } - - ChannelOptionState( - title = stringResource(id = uiData.first), - titleColor = ChatTheme.colors.textPrimary, - iconPainter = painterResource(id = uiData.second), - iconColor = ChatTheme.colors.textSecondary, - action = uiData.third, - ) } else { null } @@ -266,11 +264,24 @@ private fun buildMuteOption( @Composable private fun ChannelOptionsPreview() { ChatTheme { + val channel = PreviewChannelData.channelWithMessages ChannelOptions( - options = buildDefaultChannelOptionsState( - selectedChannel = PreviewChannelData.channelWithMessages, - isMuted = false, - ownCapabilities = ChannelCapabilities.toSet(), + actions = listOf( + ViewInfo( + channel = channel, + label = "Channel Info", + onAction = {}, + ), + MuteChannel( + channel = channel, + label = "Mute Channel", + onAction = {}, + ), + DeleteConversation( + channel = channel, + label = "Delete Conversation", + onAction = {}, + ), ), onChannelOptionClick = {}, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt index 88837dd8acf..275ccf53ea8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt @@ -72,7 +72,6 @@ import androidx.core.net.toUri import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.extensions.getCreatedAtOrThrow import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.channels.list.ChannelOptionState import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messageoptions.MessageOptionItemState @@ -101,11 +100,14 @@ import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelItemTrai import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelListEmptyContent import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelListLoadingIndicator import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelSearchEmptyContent +import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelSwipeActions import io.getstream.chat.android.compose.ui.channels.list.DefaultChannelsLoadingMoreIndicator import io.getstream.chat.android.compose.ui.channels.list.DefaultSearchResultItemCenterContent import io.getstream.chat.android.compose.ui.channels.list.DefaultSearchResultItemLeadingContent import io.getstream.chat.android.compose.ui.channels.list.DefaultSearchResultItemTrailingContent +import io.getstream.chat.android.compose.ui.channels.list.LocalSwipeRevealCoordinator import io.getstream.chat.android.compose.ui.channels.list.SearchResultItem +import io.getstream.chat.android.compose.ui.channels.list.SwipeableChannelItem import io.getstream.chat.android.compose.ui.components.DefaultSearchClearButton import io.getstream.chat.android.compose.ui.components.DefaultSearchLabel import io.getstream.chat.android.compose.ui.components.DefaultSearchLeadingIcon @@ -401,6 +403,8 @@ public interface ChatComponentFactory { /** * The default channel list item content. + * When swipe actions are enabled and a [SwipeRevealCoordinator][LocalSwipeRevealCoordinator] + * is provided, wraps the item in [SwipeableChannelItem]. */ @Composable public fun LazyItemScope.ChannelListItemContent( @@ -409,13 +413,43 @@ public interface ChatComponentFactory { onChannelClick: (Channel) -> Unit, onChannelLongClick: (Channel) -> Unit, ) { - ChannelItem( - modifier = Modifier.animateItem(), - channelItem = channelItem, - currentUser = currentUser, - onChannelClick = onChannelClick, - onChannelLongClick = onChannelLongClick, - ) + val coordinator = LocalSwipeRevealCoordinator.current + val swipeEnabled = ChatTheme.config.channelList.swipeActionsEnabled && coordinator != null + + if (swipeEnabled) { + SwipeableChannelItem( + channelCid = channelItem.channel.cid, + backgroundColor = ChatTheme.colors.backgroundCoreApp, + swipeActions = { Row { ChannelSwipeActions(channelItem) } }, + ) { + ChannelItem( + modifier = Modifier.animateItem(), + channelItem = channelItem, + currentUser = currentUser, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + ) + } + } else { + ChannelItem( + modifier = Modifier.animateItem(), + channelItem = channelItem, + currentUser = currentUser, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + ) + } + } + + /** + * The swipe actions revealed when swiping a channel list item. + * Override this to provide custom swipe actions. + * + * @param channelItem The channel item to build actions for. + */ + @Composable + public fun RowScope.ChannelSwipeActions(channelItem: ItemState.ChannelItemState) { + DefaultChannelSwipeActions(channelItem) } /** @@ -2063,25 +2097,25 @@ public interface ChatComponentFactory { * * @param modifier The modifier for the menu. * @param selectedChannel The selected channel. - * @param isMuted Whether the channel is muted. * @param currentUser The current user. - * @param onChannelOptionClick Callback for when a channel option is clicked. + * @param channelActions The list of channel actions to show in the menu. + * @param onChannelOptionClick Callback for when a channel action is clicked. * @param onDismiss Callback for when the menu is dismissed. */ @Composable public fun ChannelMenu( modifier: Modifier, selectedChannel: Channel, - isMuted: Boolean, currentUser: User?, + channelActions: List, onChannelOptionClick: (ChannelAction) -> Unit, onDismiss: () -> Unit, ) { SelectedChannelMenu( modifier = modifier, selectedChannel = selectedChannel, - isMuted = isMuted, currentUser = currentUser, + channelActions = channelActions, onChannelOptionClick = onChannelOptionClick, onDismiss = onDismiss, ) @@ -2108,17 +2142,17 @@ public interface ChatComponentFactory { /** * Factory method for creating the center content of the SelectedChannelMenu. * - * @param onChannelOptionClick Callback for when a channel option is clicked. - * @param channelOptions List of channel options. + * @param onChannelOptionClick Callback for when a channel action is clicked. + * @param channelActions List of channel actions. */ @Composable public fun ChannelMenuCenterContent( modifier: Modifier, onChannelOptionClick: (ChannelAction) -> Unit, - channelOptions: List, + channelActions: List, ) { ChannelMenuOptions( - channelOptions = channelOptions, + channelActions = channelActions, onChannelOptionClick = onChannelOptionClick, modifier = modifier, ) @@ -2127,43 +2161,50 @@ public interface ChatComponentFactory { /** * Factory method for creating the options content of the SelectedChannelMenu. * - * @param onChannelOptionClick Callback for when a channel option is clicked. - * @param channelOptions List of channel options. + * @param onChannelOptionClick Callback for when a channel action is clicked. + * @param channelActions List of channel actions. */ @Composable public fun ChannelMenuOptions( modifier: Modifier, onChannelOptionClick: (ChannelAction) -> Unit, - channelOptions: List, + channelActions: List, ) { ChannelOptions( - options = channelOptions, + actions = channelActions, onChannelOptionClick = onChannelOptionClick, modifier = modifier, ) } /** - * Factory method for creating the footer content of the SelectedChannelMenu. + * Factory method for creating a single channel option item. * - * @param modifier The modifier for the footer. + * @param modifier The modifier for the item. + * @param action The channel action to render. + * @param onClick Callback for when the item is clicked. */ @Composable public fun ChannelOptionsItem( modifier: Modifier, - option: ChannelOptionState, + action: ChannelAction, onClick: () -> Unit, ) { + val titleColor = if (action.isDestructive) { + ChatTheme.colors.accentError + } else { + ChatTheme.colors.textPrimary + } MenuOptionItem( - modifier = modifier, - title = option.title, - titleColor = option.titleColor, + modifier = modifier.padding(horizontal = StreamTokens.spacingMd), + title = action.label, + titleColor = titleColor, leadingIcon = { - ChannelOptionsItemLeadingIcon(Modifier, option) + ChannelOptionsItemLeadingIcon(Modifier, action) }, onClick = onClick, style = ChatTheme.typography.bodyDefault, - itemHeight = 56.dp, + itemHeight = 44.dp, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) @@ -2172,16 +2213,21 @@ public interface ChatComponentFactory { /** * Factory method for creating the leading icon of the Channel options menu item. * - * @param option The channel option state. + * @param action The channel action. */ @Composable - public fun ChannelOptionsItemLeadingIcon(modifier: Modifier, option: ChannelOptionState) { + public fun ChannelOptionsItemLeadingIcon(modifier: Modifier, action: ChannelAction) { + val iconColor = if (action.isDestructive) { + ChatTheme.colors.accentError + } else { + ChatTheme.colors.textSecondary + } Icon( modifier = modifier - .size(56.dp) - .padding(16.dp), - painter = option.iconPainter, - tint = option.iconColor, + .padding(end = StreamTokens.spacingXs) // 8dp gap to text + .size(StreamTokens.spacingXl), // 24dp + painter = painterResource(id = action.icon), + tint = iconColor, contentDescription = null, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt index 4f410b64805..b2bef106e1e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatConfig.kt @@ -31,9 +31,11 @@ public enum class MuteIndicatorPosition { * Behavioral configuration for the channel list. * * @param muteIndicatorPosition Where the mute icon is placed in the channel list item. + * @param swipeActionsEnabled Whether swipe-to-reveal actions are enabled on channel list items. */ public data class ChannelListConfig( val muteIndicatorPosition: MuteIndicatorPosition = MuteIndicatorPosition.INLINE_TITLE, + val swipeActionsEnabled: Boolean = true, ) /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt index 85737f71d69..bd856c58c0d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt @@ -31,6 +31,7 @@ import io.getstream.chat.android.client.api.state.GlobalState import io.getstream.chat.android.client.api.state.QueryChannelsState import io.getstream.chat.android.client.api.state.globalStateFlow import io.getstream.chat.android.client.api.state.queryChannelsAsState +import io.getstream.chat.android.client.extensions.isPinned import io.getstream.chat.android.compose.state.QueryConfig import io.getstream.chat.android.compose.state.channels.list.ChannelsState import io.getstream.chat.android.compose.state.channels.list.ItemState @@ -49,7 +50,6 @@ import io.getstream.chat.android.models.TypingEvent import io.getstream.chat.android.models.User import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.models.querysort.QuerySorter -import io.getstream.chat.android.ui.common.state.channels.actions.Cancel import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction import io.getstream.chat.android.ui.common.utils.extensions.defaultChannelListFilter import io.getstream.log.taggedLogger @@ -618,25 +618,29 @@ public class ChannelListViewModel( } /** - * Clears the active action if we've chosen [Cancel], otherwise, stores the selected action as - * the currently active action, in [activeChannelAction]. + * Executes the given [ChannelAction] immediately if it doesn't require confirmation, + * or stores it as [activeChannelAction] to show a confirmation dialog. * - * It also removes the [selectedChannel] if the action is [Cancel]. - * - * @param channelAction The selected action. + * @param action The action to execute or confirm. */ - public fun performChannelAction(channelAction: ChannelAction) { - if (channelAction is Cancel) { - selectedChannel.value = null - } - - activeChannelAction = if (channelAction == Cancel) { - null + public fun executeOrConfirm(action: ChannelAction) { + if (action.confirmationPopup != null) { + activeChannelAction = action } else { - channelAction + action.onAction() + dismissChannelAction() } } + /** + * Executes the currently pending [activeChannelAction] after user confirmation + * and dismisses it from the UI. + */ + public fun confirmPendingAction() { + activeChannelAction?.onAction?.invoke() + dismissChannelAction() + } + /** * Mutes a channel. * @@ -747,6 +751,7 @@ public class ChannelListViewModel( ItemState.ChannelItemState( channel = it, isMuted = it.cid in mutedChannelIds, + isPinned = it.isPinned(), typingUsers = typingEvents[it.cid]?.users ?: emptyList(), draftMessage = draftMessages[it.cid], ) diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_add.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_add.xml index ab429e4e5b5..e36ba0fb939 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_add.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_add.xml @@ -15,20 +15,11 @@ limitations under the License. --> - + android:width="24dp" + android:height="24dp" + android:viewportWidth="16" + android:viewportHeight="16"> - + android:fillColor="#FF000000" + android:pathData="M7.40039,13.5V8.59961H2.5C2.16863,8.59961 1.90039,8.33137 1.90039,8C1.90039,7.66863 2.16863,7.40039 2.5,7.40039H7.40039V2.5C7.40039,2.16863 7.66863,1.90039 8,1.90039C8.33137,1.90039 8.59961,2.16863 8.59961,2.5V7.40039H13.5C13.8314,7.40039 14.0996,7.66863 14.0996,8C14.0996,8.33137 13.8314,8.59961 13.5,8.59961H8.59961V13.5C8.59961,13.8314 8.33137,14.0996 8,14.0996C7.66863,14.0996 7.40039,13.8314 7.40039,13.5Z" /> diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_clock.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_clock.xml index 74c38c6fdc2..37cf3f14177 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_clock.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_clock.xml @@ -20,9 +20,7 @@ android:viewportWidth="16" android:viewportHeight="16"> + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M8.00004,14.6666C11.6819,14.6666 14.6667,11.6818 14.6667,7.99998C14.6667,4.31808 11.6819,1.33331 8.00004,1.33331C4.31814,1.33331 1.33337,4.31808 1.33337,7.99998C1.33337,11.6818 4.31814,14.6666 8.00004,14.6666ZM8.50004,5.16665C8.50004,4.89051 8.27617,4.66665 8.00004,4.66665C7.72391,4.66665 7.50004,4.89051 7.50004,5.16665V7.99998C7.50004,8.13258 7.55271,8.25978 7.64651,8.35351L9.47984,10.1868C9.67511,10.3821 9.99164,10.3821 10.1869,10.1868C10.3822,9.99158 10.3822,9.67505 10.1869,9.47978L8.50004,7.79285V5.16665Z" /> diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_error.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_error.xml index 6d6fd934674..66d4c1cbd2f 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_error.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_error.xml @@ -17,11 +17,10 @@ + android:viewportWidth="16" + android:viewportHeight="16"> + android:fillColor="#FF000000" + android:fillType="evenOdd" + android:pathData="M8.00004,1.33331C11.6819,1.33331 14.6667,4.31808 14.6667,7.99998C14.6667,11.6818 11.6819,14.6666 8.00004,14.6666C4.31814,14.6666 1.33337,11.6818 1.33337,7.99998C1.33337,4.31808 4.31814,1.33331 8.00004,1.33331ZM8.00004,9.66665C7.63184,9.66665 7.33337,9.96511 7.33337,10.3333C7.33345,10.7014 7.63189,11 8.00004,11C8.3682,11 8.66663,10.7014 8.66671,10.3333C8.66671,9.96511 8.36824,9.66665 8.00004,9.66665ZM8.00004,5.33331C7.72391,5.33331 7.50004,5.55717 7.50004,5.83331V8.49998C7.50011,8.77605 7.72395,8.99998 8.00004,8.99998C8.27613,8.99998 8.49997,8.77605 8.50004,8.49998V5.83331C8.50004,5.55717 8.27617,5.33331 8.00004,5.33331Z" /> diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_more_options.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_more_options.xml new file mode 100644 index 00000000000..f54561e9b7a --- /dev/null +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_more_options.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_muted.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_muted.xml index e891c5f4f9a..15a6526a235 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_muted.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_muted.xml @@ -19,8 +19,11 @@ android:height="16dp" android:viewportWidth="16" android:viewportHeight="16"> - + + diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_seen.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_seen.xml index dffb9f8cd18..4f6d76d71a9 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_seen.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_seen.xml @@ -20,9 +20,6 @@ android:viewportWidth="16" android:viewportHeight="16"> + android:fillColor="#FF000000" + android:pathData="M9.74121,3.43164C9.9169,3.15085 10.2874,3.06564 10.5684,3.24121C10.8492,3.41691 10.9344,3.78745 10.7588,4.06836L5.44629,12.5684C5.34945,12.7231 5.18722,12.8249 5.00586,12.8457C4.82419,12.8664 4.64297,12.8031 4.51367,12.6738L1.32617,9.48633C1.09186,9.25202 1.09186,8.87299 1.32617,8.63868C1.56049,8.40436 1.93951,8.40436 2.17383,8.63868L4.82715,11.292L9.74121,3.43164ZM13.7412,3.43164C13.9169,3.15085 14.2874,3.06564 14.5684,3.24121C14.8492,3.41691 14.9344,3.78745 14.7588,4.06836L9.44629,12.5684C9.2706,12.8492 8.90006,12.9344 8.61914,12.7588C8.33835,12.5831 8.25314,12.2126 8.42871,11.9316L13.7412,3.43164Z" /> diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_sent.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_sent.xml index c721d5edb3c..4c300f2cc6e 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_sent.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_message_sent.xml @@ -20,9 +20,6 @@ android:viewportWidth="16" android:viewportHeight="16"> + android:fillColor="#FF000000" + android:pathData="M11.7412,3.43164C11.9169,3.15085 12.2874,3.06564 12.5684,3.24121C12.8492,3.41691 12.9344,3.78745 12.7588,4.06836L7.44629,12.5684C7.34945,12.7231 7.18722,12.8249 7.00586,12.8457C6.82419,12.8664 6.64297,12.8031 6.51367,12.6738L3.32617,9.48633C3.09186,9.25202 3.09186,8.87299 3.32617,8.63867C3.56049,8.40436 3.93951,8.40436 4.17383,8.63867L6.82715,11.292L11.7412,3.43164Z" /> diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 45627755bbe..015f220ca19 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -33,6 +33,16 @@ %d people are typing No channels available + + + Pin + Unpin + Mute + Unmute + Delete + More + Archive + Unarchive No results for \"%1$s\" diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/SelectedChannelMenuTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/SelectedChannelMenuTest.kt index 9d7a2bb067f..4bc7a3b2dd9 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/SelectedChannelMenuTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/SelectedChannelMenuTest.kt @@ -28,6 +28,7 @@ import io.getstream.chat.android.compose.util.extensions.toSet import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.previewdata.PreviewChannelData import io.getstream.chat.android.previewdata.PreviewUserData +import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo import org.junit.Rule import org.junit.Test @@ -39,14 +40,17 @@ internal class SelectedChannelMenuTest : PaparazziComposeTest { @Test fun `selected channel`() { snapshot { + val channel = PreviewChannelData.channelWithManyMembers.copy( + ownCapabilities = ChannelCapabilities.toSet(), + ) Box(modifier = Modifier.fillMaxSize()) { SelectedChannelMenu( modifier = Modifier.align(Alignment.BottomCenter), - selectedChannel = PreviewChannelData.channelWithManyMembers.copy( - ownCapabilities = ChannelCapabilities.toSet(), - ), - isMuted = false, + selectedChannel = channel, currentUser = PreviewUserData.user1, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), onChannelOptionClick = {}, onDismiss = {}, ) @@ -57,14 +61,17 @@ internal class SelectedChannelMenuTest : PaparazziComposeTest { @Test fun `selected channel in dark mode`() { snapshot(isInDarkMode = true) { + val channel = PreviewChannelData.channelWithManyMembers.copy( + ownCapabilities = ChannelCapabilities.toSet(), + ) Box(modifier = Modifier.fillMaxSize()) { SelectedChannelMenu( modifier = Modifier.align(Alignment.BottomCenter), - selectedChannel = PreviewChannelData.channelWithManyMembers.copy( - ownCapabilities = ChannelCapabilities.toSet(), - ), - isMuted = false, + selectedChannel = channel, currentUser = PreviewUserData.user1, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), onChannelOptionClick = {}, onDismiss = {}, ) diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt index faf5b675528..cb9e9161359 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt @@ -115,8 +115,13 @@ internal class ChannelListViewModelTest { .get(this) viewModel.selectChannel(channel1) - viewModel.performChannelAction(DeleteConversation(channel1)) - viewModel.deleteConversation(channel1) + viewModel.executeOrConfirm( + DeleteConversation( + channel = channel1, + label = "Delete", + onAction = { viewModel.deleteConversation(channel1) }, + ), + ) viewModel.activeChannelAction `should be equal to` null viewModel.selectedChannel.value `should be equal to` null diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/channels/SelectedChannelMenu.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/channels/SelectedChannelMenu.kt index a21525bb861..76569a505aa 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/channels/SelectedChannelMenu.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/channels/SelectedChannelMenu.kt @@ -18,10 +18,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenu +import io.getstream.chat.android.compose.ui.components.channels.buildDefaultChannelActions import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channels.ChannelViewModelFactory -import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo /** * [Usage](https://getstream.io/chat/docs/sdk/android/compose/channel-components/selected-channel-menu/#usage) @@ -44,7 +44,13 @@ private object SelectedChannelMenuUsageSnippet { // The rest of your UI if (currentlySelectedChannel != null) { - val isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid) + val channelActions = buildDefaultChannelActions( + selectedChannel = currentlySelectedChannel, + isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), + ownCapabilities = currentlySelectedChannel.ownCapabilities, + viewModel = listViewModel, + onViewInfoAction = {}, + ) SelectedChannelMenu( modifier = Modifier @@ -52,9 +58,9 @@ private object SelectedChannelMenuUsageSnippet { .wrapContentHeight() // Wrap height .align(Alignment.BottomCenter), // Aligning the content to the bottom selectedChannel = currentlySelectedChannel, - isMuted = isMuted, currentUser = user, - onChannelOptionClick = { listViewModel.performChannelAction(it) }, + channelActions = channelActions, + onChannelOptionClick = { listViewModel.executeOrConfirm(it) }, onDismiss = { listViewModel.dismissChannelAction() } ) } @@ -86,7 +92,15 @@ private object SelectedChannelMenuHandlingActionsSnippet { // The rest of your UI if (currentlySelectedChannel != null) { - val isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid) + val channelActions = buildDefaultChannelActions( + selectedChannel = currentlySelectedChannel, + isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), + ownCapabilities = currentlySelectedChannel.ownCapabilities, + viewModel = listViewModel, + onViewInfoAction = { channel -> + // Start the channel info screen + }, + ) SelectedChannelMenu( modifier = Modifier @@ -94,14 +108,10 @@ private object SelectedChannelMenuHandlingActionsSnippet { .wrapContentHeight() // Wrap height .align(Alignment.BottomCenter), // Aligning the content to the bottom selectedChannel = currentlySelectedChannel, - isMuted = isMuted, currentUser = user, + channelActions = channelActions, onChannelOptionClick = { action -> - if (action is ViewInfo) { - // Start the channel info screen - } else { - listViewModel.performChannelAction(action) - } + listViewModel.executeOrConfirm(action) }, onDismiss = { listViewModel.dismissChannelAction() } ) @@ -134,7 +144,13 @@ private object SelectedChannelMenuCustomizationSnippet { // The rest of your UI if (currentlySelectedChannel != null) { - val isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid) + val channelActions = buildDefaultChannelActions( + selectedChannel = currentlySelectedChannel, + isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), + ownCapabilities = currentlySelectedChannel.ownCapabilities, + viewModel = listViewModel, + onViewInfoAction = {}, + ) SelectedChannelMenu( modifier = Modifier @@ -144,9 +160,9 @@ private object SelectedChannelMenuCustomizationSnippet { .align(Alignment.Center), // Centering the component shape = RoundedCornerShape(16.dp), // Rounded corners for all sides selectedChannel = currentlySelectedChannel, - isMuted = isMuted, currentUser = user, - onChannelOptionClick = { listViewModel.performChannelAction(it) }, + channelActions = channelActions, + onChannelOptionClick = { listViewModel.executeOrConfirm(it) }, onDismiss = { listViewModel.dismissChannelAction() } ) } diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index f8508c9c22e..798837ef3d6 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -1630,122 +1630,144 @@ public final class io/getstream/chat/android/ui/common/state/channel/info/Channe public final class io/getstream/chat/android/ui/common/state/channels/actions/ArchiveChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/ArchiveChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/ArchiveChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/ArchiveChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } -public final class io/getstream/chat/android/ui/common/state/channels/actions/Cancel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { - public static final field $stable I - public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/channels/actions/Cancel; - public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun toString ()Ljava/lang/String; +public abstract interface class io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { + public abstract fun getChannel ()Lio/getstream/chat/android/models/Channel; + public abstract fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public abstract fun getIcon ()I + public abstract fun getLabel ()Ljava/lang/String; + public abstract fun getOnAction ()Lkotlin/jvm/functions/Function0; + public abstract fun getRequiredCapability ()Ljava/lang/String; + public abstract fun isDestructive ()Z } -public abstract class io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { - public static final field $stable I - public abstract fun getChannel ()Lio/getstream/chat/android/models/Channel; +public final class io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction$DefaultImpls { + public static fun getConfirmationPopup (Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;)Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public static fun getRequiredCapability (Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;)Ljava/lang/String; + public static fun isDestructive (Lio/getstream/chat/android/ui/common/state/channels/actions/ChannelAction;)Z } -public final class io/getstream/chat/android/ui/common/state/channels/actions/DeleteConversation : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { +public final class io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/DeleteConversation; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/DeleteConversation;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/DeleteConversation; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; public fun equals (Ljava/lang/Object;)Z - public fun getChannel ()Lio/getstream/chat/android/models/Channel; + public final fun getConfirmButtonText ()Ljava/lang/String; + public final fun getMessage ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/state/channels/actions/DeleteConversation : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup;)V + public synthetic fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getChannel ()Lio/getstream/chat/android/models/Channel; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z +} + public final class io/getstream/chat/android/ui/common/state/channels/actions/LeaveGroup : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/LeaveGroup; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/LeaveGroup;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/LeaveGroup; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup;)V + public synthetic fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/MuteChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/MuteChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/MuteChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/MuteChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/PinChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/PinChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/PinChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/PinChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/UnarchiveChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnarchiveChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/UnarchiveChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnarchiveChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/UnmuteChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnmuteChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/UnmuteChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnmuteChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/UnpinChannel : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnpinChannel; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/UnpinChannel;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/UnpinChannel; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public final class io/getstream/chat/android/ui/common/state/channels/actions/ViewInfo : io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;)V - public final fun component1 ()Lio/getstream/chat/android/models/Channel; - public final fun copy (Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/channels/actions/ViewInfo; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/channels/actions/ViewInfo;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/channels/actions/ViewInfo; - public fun equals (Ljava/lang/Object;)Z + public fun (Lio/getstream/chat/android/models/Channel;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public fun getChannel ()Lio/getstream/chat/android/models/Channel; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public fun getConfirmationPopup ()Lio/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup; + public fun getIcon ()I + public fun getLabel ()Ljava/lang/String; + public fun getOnAction ()Lkotlin/jvm/functions/Function0; + public fun getRequiredCapability ()Ljava/lang/String; + public fun isDestructive ()Z } public abstract interface class io/getstream/chat/android/ui/common/state/mentions/MentionListEvent { diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt index 57dec15ace8..9abe4b2a6a5 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ChannelAction.kt @@ -16,66 +16,147 @@ package io.getstream.chat.android.ui.common.state.channels.actions +import androidx.annotation.DrawableRes import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.ui.common.R /** - * Represents the list of actions users can take with selected channels. + * A self-describing channel action that carries display info, capability requirements, and an + * execution handler. Works for swipe actions, options menus, and any future action surface. * - * @property channel The selected channel. + * @property channel The channel this action targets. + * @property icon Drawable resource for the action icon. + * @property label Human-readable label for the action. + * @property requiredCapability Optional channel capability required to show this action. + * @property confirmationPopup Optional confirmation dialog to show before executing. + * @property isDestructive Whether this action is destructive (e.g. delete). + * @property onAction The handler to execute when the action is confirmed. */ -public sealed class ChannelAction { - public abstract val channel: Channel +public interface ChannelAction { + public val channel: Channel + + @get:DrawableRes + public val icon: Int + public val label: String + public val requiredCapability: String? get() = null + public val confirmationPopup: ConfirmationPopup? get() = null + public val isDestructive: Boolean get() = false + public val onAction: () -> Unit } /** * Show more info about the channel. */ -public data class ViewInfo(override val channel: Channel) : ChannelAction() - -/** - * Shows a dialog to leave the group. - */ -public data class LeaveGroup(override val channel: Channel) : ChannelAction() +public class ViewInfo( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_view_info +} /** - * Mutes the channel. + * Leave the group channel. */ -public data class MuteChannel(override val channel: Channel) : ChannelAction() +public class LeaveGroup( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, + override val confirmationPopup: ConfirmationPopup? = null, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_leave + override val requiredCapability: String = ChannelCapabilities.LEAVE_CHANNEL + override val isDestructive: Boolean = true +} /** - * Unmutes the channel. + * Mute the channel. */ -public data class UnmuteChannel(override val channel: Channel) : ChannelAction() +public class MuteChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_mute + override val requiredCapability: String = ChannelCapabilities.MUTE_CHANNEL +} /** - * Shows a dialog to delete the conversation, if we have the permission. + * Unmute the channel. */ -public data class DeleteConversation(override val channel: Channel) : ChannelAction() +public class UnmuteChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_unmute + override val requiredCapability: String = ChannelCapabilities.MUTE_CHANNEL +} /** - * Shows a dialog to pin the channel. + * Delete the conversation. */ -public data class PinChannel(override val channel: Channel) : ChannelAction() +public class DeleteConversation( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, + override val confirmationPopup: ConfirmationPopup? = null, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_delete + override val requiredCapability: String = ChannelCapabilities.DELETE_CHANNEL + override val isDestructive: Boolean = true +} /** - * Shows a dialog to unpin the channel. + * Pin the channel. */ -public data class UnpinChannel(override val channel: Channel) : ChannelAction() +public class PinChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_pin +} /** - * Shows a dialog to archive the channel. + * Unpin the channel. */ -public data class ArchiveChannel(override val channel: Channel) : ChannelAction() +public class UnpinChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_unpin +} /** - * Shows a dialog to unarchive the channel. + * Archive the channel. */ -public data class UnarchiveChannel(override val channel: Channel) : ChannelAction() +public class ArchiveChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_archive +} /** - * Dismisses the actions. + * Unarchive the channel. */ -public object Cancel : ChannelAction() { - override val channel: Channel = Channel() - override fun toString(): String = "Cancel" +public class UnarchiveChannel( + override val channel: Channel, + override val label: String, + override val onAction: () -> Unit, +) : ChannelAction { + @DrawableRes + override val icon: Int = R.drawable.stream_ic_action_unarchive } diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt new file mode 100644 index 00000000000..2a827eedd7a --- /dev/null +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/channels/actions/ConfirmationPopup.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.ui.common.state.channels.actions + +/** + * Represents a confirmation popup that should be shown before executing a [ChannelAction]. + * + * @property title The title of the confirmation dialog. + * @property message The message body of the confirmation dialog. + * @property confirmButtonText The text for the positive/confirm button. + */ +public data class ConfirmationPopup( + val title: String, + val message: String, + val confirmButtonText: String, +) diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_archive.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_archive.xml new file mode 100644 index 00000000000..cbbd91b57c5 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_archive.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_delete.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_delete.xml new file mode 100644 index 00000000000..0d3c615ae87 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_delete.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_leave.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_leave.xml new file mode 100644 index 00000000000..c687cead6fa --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_leave.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_more.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_more.xml new file mode 100644 index 00000000000..f54561e9b7a --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_more.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_mute.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_mute.xml new file mode 100644 index 00000000000..980937e003f --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_mute.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_pin.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_pin.xml new file mode 100644 index 00000000000..05f29cc7239 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_pin.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unarchive.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unarchive.xml new file mode 100644 index 00000000000..6fc54afb332 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unarchive.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unmute.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unmute.xml new file mode 100644 index 00000000000..afbafe0dc56 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unmute.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unpin.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unpin.xml new file mode 100644 index 00000000000..ad378670cff --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_unpin.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_view_info.xml b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_view_info.xml new file mode 100644 index 00000000000..fd381147c4e --- /dev/null +++ b/stream-chat-android-ui-common/src/main/res/drawable/stream_ic_action_view_info.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt index 5e228599df4..b4a66ace000 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelActionsDialogFragment.kt @@ -34,6 +34,7 @@ import io.getstream.chat.android.ui.common.utils.extensions.isDirectMessaging import io.getstream.chat.android.ui.databinding.StreamUiFragmentChannelActionsBinding import io.getstream.chat.android.ui.feature.channels.actions.ChannelActionsDialogViewStyle import io.getstream.chat.android.ui.font.setTextStyle +import io.getstream.chat.android.ui.utils.extensions.getDrawableCompat import io.getstream.chat.android.ui.utils.extensions.getMembersStatusText import io.getstream.chat.android.ui.utils.extensions.isCurrentUser import io.getstream.chat.android.ui.utils.extensions.setStartDrawable @@ -155,20 +156,22 @@ internal class ChannelActionsDialogFragment : BottomSheetDialogFragment() { selectedChannel = channel, ownCapabilities = channel.ownCapabilities, style = style, - ).forEach { option -> + ).forEach { action -> val channelOptionTextView = requireContext().streamThemeInflater.inflate( R.layout.stream_ui_channel_option_item, binding.optionsContainer, false, ) as TextView - channelOptionTextView.text = option.optionText - channelOptionTextView.setStartDrawable(option.optionIcon) + channelOptionTextView.text = action.label + channelOptionTextView.setStartDrawable( + requireContext().getDrawableCompat(action.icon)!!, + ) channelOptionTextView.setOnClickListener { - channelOptionClickListener?.onChannelOptionClick(option.channelAction) + channelOptionClickListener?.onChannelOptionClick(action) dismiss() } channelOptionTextView.setTextStyle( - if (option.isWarningItem) { + if (action.isDestructive) { style.warningItemTextStyle } else { style.itemTextStyle diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItem.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItem.kt deleted file mode 100644 index efff4edddea..00000000000 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItem.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.chat.android.ui.feature.channels.actions.internal - -import android.graphics.drawable.Drawable -import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction - -/** - * UI representation of a Channel option, when the user selects a channel in the list. - * - * @param optionText The text of the option item. - * @param optionIcon The icon of the option item. - * @param channelAction The [ChannelAction] the option represents. - * @param isWarningItem If the option item is dangerous. - */ -internal data class ChannelOptionItem( - val optionIcon: Drawable, - val optionText: String, - val channelAction: ChannelAction, - val isWarningItem: Boolean = false, -) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItemsFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItemsFactory.kt index 639f28156e1..f92f2b84f00 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItemsFactory.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/actions/internal/ChannelOptionItemsFactory.kt @@ -20,31 +20,30 @@ import android.content.Context import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.ui.R -import io.getstream.chat.android.ui.common.state.channels.actions.Cancel +import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction import io.getstream.chat.android.ui.common.state.channels.actions.DeleteConversation import io.getstream.chat.android.ui.common.state.channels.actions.LeaveGroup import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo import io.getstream.chat.android.ui.feature.channels.actions.ChannelActionsDialogViewStyle -import io.getstream.chat.android.ui.utils.extensions.getDrawableCompat /** - * An interface that allows the creation of channel option items. + * An interface that allows the creation of channel action items. */ internal interface ChannelOptionItemsFactory { /** - * Creates [ChannelOptionItem]s for the selected channel. + * Creates [ChannelAction]s for the selected channel. * * @param selectedChannel The currently selected channel. * @param ownCapabilities Set of capabilities the user is given for the current channel. * @param style The style of the dialog. - * @return The list of channel option items to display. + * @return The list of channel actions to display. */ fun createChannelOptionItems( selectedChannel: Channel, ownCapabilities: Set, style: ChannelActionsDialogViewStyle, - ): List + ): List companion object { /** @@ -68,55 +67,49 @@ internal open class DefaultChannelOptionItemsFactory( ) : ChannelOptionItemsFactory { /** - * Creates [ChannelOptionItem]s for the selected channel. + * Creates [ChannelAction]s for the selected channel. + * + * [ChannelAction.onAction] is left empty because the XML module dispatches + * actions through [ChannelActionsDialogFragment.ChannelOptionClickListener], + * not through the action's own handler. * * @param selectedChannel The currently selected channel. * @param ownCapabilities Set of capabilities the user is given for the current channel. * @param style The style of the dialog. - * @return The list of channel option items to display. + * @return The list of channel actions to display. */ override fun createChannelOptionItems( selectedChannel: Channel, ownCapabilities: Set, style: ChannelActionsDialogViewStyle, - ): List { + ): List { val canLeaveChannel = ownCapabilities.contains(ChannelCapabilities.LEAVE_CHANNEL) val canDeleteChannel = ownCapabilities.contains(ChannelCapabilities.DELETE_CHANNEL) return listOfNotNull( if (style.viewInfoEnabled) { - ChannelOptionItem( - optionText = context.getString(R.string.stream_ui_channel_list_view_info), - optionIcon = context.getDrawableCompat(R.drawable.stream_ui_ic_single_user)!!, - channelAction = ViewInfo(selectedChannel), + ViewInfo( + channel = selectedChannel, + label = context.getString(R.string.stream_ui_channel_list_view_info), + onAction = {}, ) } else { null }, if (style.leaveGroupEnabled && canLeaveChannel) { - ChannelOptionItem( - optionText = context.getString(R.string.stream_ui_channel_list_leave_channel), - optionIcon = context.getDrawableCompat(R.drawable.stream_ui_ic_leave_group)!!, - channelAction = LeaveGroup(selectedChannel), + LeaveGroup( + channel = selectedChannel, + label = context.getString(R.string.stream_ui_channel_list_leave_channel), + onAction = {}, ) } else { null }, if (style.deleteConversationEnabled && canDeleteChannel) { - ChannelOptionItem( - optionText = context.getString(R.string.stream_ui_channel_list_delete_channel), - optionIcon = context.getDrawableCompat(R.drawable.stream_ui_ic_delete)!!, - channelAction = DeleteConversation(selectedChannel), - isWarningItem = true, - ) - } else { - null - }, - if (style.cancelEnabled) { - ChannelOptionItem( - optionText = context.getString(R.string.stream_ui_channel_list_dismiss_dialog), - optionIcon = context.getDrawableCompat(R.drawable.stream_ui_ic_clear)!!, - channelAction = Cancel, + DeleteConversation( + channel = selectedChannel, + label = context.getString(R.string.stream_ui_channel_list_delete_channel), + onAction = {}, ) } else { null