diff --git a/.gitignore b/.gitignore index ad21726cf67..77e6f64ab16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.iml +.planning/ .composite buildSrc/build .gradle diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt index d8ffed6cf53..c70cb1a0a93 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt @@ -20,6 +20,7 @@ import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelList.Chann import io.getstream.chat.android.compose.uiautomator.isDisplayed import io.getstream.chat.android.compose.uiautomator.waitToAppear import io.getstream.chat.android.compose.uiautomator.waitToDisappear +import io.getstream.chat.android.e2e.test.robots.ParticipantRobot import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -30,7 +31,11 @@ fun UserRobot.assertChannelAvatar(): UserRobot { } fun UserRobot.assertMessageInChannelPreview(text: String, fromCurrentUser: Boolean? = null): UserRobot { - val expectedPreview = if (fromCurrentUser == true) "You: $text" else text + val expectedPreview = when (fromCurrentUser) { + true -> "You: $text" + false -> "${ParticipantRobot.name}: $text" + null -> text + } assertEquals(expectedPreview, Channel.messagePreview.waitToAppear().text.trimEnd()) return this } diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelListTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelListTests.kt index fa8f1025c00..d1897cbbaeb 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelListTests.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/ChannelListTests.kt @@ -172,7 +172,7 @@ class ChannelListTests : StreamTestCase() { @AllureId("5821") @Test - fun test_channelPreviewShowsPreviousMessage_whenLastMessageIsDeleted() { + fun test_channelPreviewShowsDeletedMessage_whenLastMessageIsDeleted() { val oldMessage = "Old" val newMessage = "New" @@ -190,8 +190,8 @@ class ChannelListTests : StreamTestCase() { step("WHEN user goes back to the channel list") { userRobot.tapOnBackButton() } - step("THEN the channel preview shows previous message") { - userRobot.assertMessageInChannelPreview(oldMessage, fromCurrentUser = false) + step("THEN the channel preview shows 'Message deleted'") { + userRobot.assertMessageInChannelPreview("Message deleted", fromCurrentUser = false) } step("AND the message timestamp is shown") { userRobot.assertMessagePreviewTimestamp(isDisplayed = true) 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 ad3333ff8dc..9216d8090ab 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.ChatConfig @@ -381,6 +382,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) @@ -388,10 +396,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, + onChannelOptionConfirm = { 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..49249925865 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 @@ -25,6 +25,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 +44,37 @@ 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 pinnedModifier = if (channelItem.channel.isPinned()) { + Modifier.background(color = ChatTheme.colors.backgroundCoreHighlight) + } else { + Modifier + } + + if (swipeEnabled) { + SwipeableChannelItem( + modifier = Modifier.animateItem(), + channelCid = channelItem.channel.cid, + backgroundColor = ChatTheme.colors.backgroundCoreApp, + swipeActions = { ChannelSwipeActions(channelItem) }, + ) { + ChannelItem( + modifier = pinnedModifier, + channelItem = channelItem, + currentUser = currentUser, + onChannelClick = onChannelClick, + onChannelLongClick = onChannelLongClick, + ) + } + } else { + ChannelItem( + modifier = Modifier.animateItem().then(pinnedModifier), + 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 d9c40532d70..49bd3add4d8 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,14 +79,15 @@ 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;)V - public synthetic fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + 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 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 copy (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;)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;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; + 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 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; @@ -104,6 +95,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 isSelected ()Z public fun toString ()Ljava/lang/String; } @@ -1105,7 +1097,7 @@ public final class io/getstream/chat/android/compose/ui/channel/info/GroupChanne } public final class io/getstream/chat/android/compose/ui/channels/ChannelsScreenKt { - public static final fun ChannelsScreen (Lio/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/compose/ui/channels/SearchMode;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V + public static final fun ChannelsScreen (Lio/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/compose/ui/channels/SearchMode;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/channels/SearchMode : java/lang/Enum { @@ -1150,7 +1142,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 { @@ -1170,6 +1162,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/ChannelsKt public final class io/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelItemKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelItemKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-10 Lkotlin/jvm/functions/Function2; public static field lambda-2 Lkotlin/jvm/functions/Function2; public static field lambda-3 Lkotlin/jvm/functions/Function2; public static field lambda-4 Lkotlin/jvm/functions/Function2; @@ -1177,8 +1170,10 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public static field lambda-6 Lkotlin/jvm/functions/Function2; public static field lambda-7 Lkotlin/jvm/functions/Function2; public static field lambda-8 Lkotlin/jvm/functions/Function2; + public static field lambda-9 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-10$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; @@ -1186,6 +1181,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public final fun getLambda-6$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-7$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-8$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-9$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelListKt { @@ -1196,6 +1192,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public static field lambda-4 Lkotlin/jvm/functions/Function3; public static field lambda-5 Lkotlin/jvm/functions/Function3; public static field lambda-6 Lkotlin/jvm/functions/Function3; + public static field lambda-7 Lkotlin/jvm/functions/Function4; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; @@ -1203,6 +1200,7 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-5$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-6$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-7$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; } public final class io/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelsKt { @@ -1223,10 +1221,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 (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; @@ -1478,7 +1519,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 { @@ -2671,6 +2712,22 @@ public final class io/getstream/chat/android/compose/ui/theme/AttachmentPickerCo public fun toString ()Ljava/lang/String; } +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;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 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; +} + public final class io/getstream/chat/android/compose/ui/theme/ChannelMediaAttachmentsPreviewBottomBarParams { public static final field $stable I public fun (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V @@ -2740,7 +2797,7 @@ public abstract interface class io/getstream/chat/android/compose/ui/theme/ChatC public abstract fun ChannelItemTrailingContent (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelItemUnreadCountIndicator (ILandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelListDividerItem (Landroidx/compose/foundation/lazy/LazyItemScope;Landroidx/compose/runtime/Composer;I)V - public abstract fun ChannelListEmptyContent (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V + public abstract fun ChannelListEmptyContent (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelListEmptySearchContent (Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelListHeader (Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/ConnectionState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public abstract fun ChannelListHeaderCenterContent (Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/models/ConnectionState;Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V @@ -2762,12 +2819,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 (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 @@ -2931,7 +2989,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public static fun ChannelItemTrailingContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/User;Landroidx/compose/runtime/Composer;I)V public static fun ChannelItemUnreadCountIndicator (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;ILandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V public static fun ChannelListDividerItem (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/lazy/LazyItemScope;Landroidx/compose/runtime/Composer;I)V - public static fun ChannelListEmptyContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V + public static fun ChannelListEmptyContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public static fun ChannelListEmptySearchContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V public static fun ChannelListHeader (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/ConnectionState;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V public static fun ChannelListHeaderCenterContent (Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Landroidx/compose/foundation/layout/RowScope;Lio/getstream/chat/android/models/ConnectionState;Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V @@ -2953,12 +3011,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;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 @@ -3091,17 +3150,19 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatComponentFacto public final class io/getstream/chat/android/compose/ui/theme/ChatConfig { public static final field $stable I public fun ()V - public fun (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;)V - public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;)V + public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/compose/ui/theme/TranslationConfig; public final fun component2 ()Lio/getstream/chat/android/compose/ui/theme/MessageListConfig; public final fun component3 ()Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig; public final fun component4 ()Lio/getstream/chat/android/compose/ui/theme/ComposerConfig; - public final fun component5 ()Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig; - public final fun copy (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChatConfig;Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; + public final fun component5 ()Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public final fun component6 ()Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig; + public final fun copy (Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/ChatConfig;Lio/getstream/chat/android/compose/ui/theme/TranslationConfig;Lio/getstream/chat/android/compose/ui/theme/MessageListConfig;Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig;Lio/getstream/chat/android/compose/ui/theme/ComposerConfig;Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChatConfig; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentPicker ()Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerConfig; + public final fun getChannelList ()Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; public final fun getComposer ()Lio/getstream/chat/android/compose/ui/theme/ComposerConfig; public final fun getMediaGallery ()Lio/getstream/chat/android/compose/ui/theme/MediaGalleryConfig; public final fun getMessageList ()Lio/getstream/chat/android/compose/ui/theme/MessageListConfig; @@ -3504,6 +3565,14 @@ public final class io/getstream/chat/android/compose/ui/theme/MessageReactionsPa public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition : java/lang/Enum { + public static final field INLINE_TITLE Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static final field TRAILING_BOTTOM Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; + public static fun values ()[Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; +} + public final class io/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme$Companion; @@ -3922,13 +3991,6 @@ public final class io/getstream/chat/android/compose/ui/util/ChannelUtilsKt { public static final fun isOneToOne (Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;)Z } -public final class io/getstream/chat/android/compose/ui/util/ComposableSingletons$MessagePreviewIconFactoryKt { - public static final field INSTANCE Lio/getstream/chat/android/compose/ui/util/ComposableSingletons$MessagePreviewIconFactoryKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; -} - public final class io/getstream/chat/android/compose/ui/util/ComposableSingletons$SnackbarPopupKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/util/ComposableSingletons$SnackbarPopupKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -3984,7 +4046,7 @@ public final class io/getstream/chat/android/compose/ui/util/MessageListUtilsKt public abstract interface class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter { public static final field Companion Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$Companion; public abstract fun formatDraftMessagePreview (Lio/getstream/chat/android/models/DraftMessage;)Landroidx/compose/ui/text/AnnotatedString; - public abstract fun formatMessagePreview (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;)Landroidx/compose/ui/text/AnnotatedString; + public abstract fun formatMessagePreview (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Z)Landroidx/compose/ui/text/AnnotatedString; public abstract fun formatMessageTitle (Lio/getstream/chat/android/models/Message;)Landroidx/compose/ui/text/AnnotatedString; } @@ -4203,8 +4265,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; @@ -4217,7 +4281,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 fa794a81616..f3006cd7335 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 @@ -34,12 +34,14 @@ public sealed class ItemState { * @param isMuted If the channel is muted 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). */ public data class ChannelItemState( val channel: Channel, val isMuted: Boolean = false, val typingUsers: List = emptyList(), val draftMessage: DraftMessage? = null, + val isSelected: Boolean = false, ) : ItemState() { override val key: String = channel.cid } 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 510d74337e0..d46a0e5fb3f 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,26 +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 /** @@ -76,6 +68,8 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo * @param searchMode The search mode for the screen. * @param onHeaderActionClick Handler for the default header action. * @param onHeaderAvatarClick Handle for when the user clicks on the header avatar. + * @param onStartChatClick Handler for the "Start a chat" button in the empty state. + * If null, the button is hidden. Defaults to null. * @param onChannelClick Handler for Channel item clicks. * @param onViewChannelInfoAction Handler for when the user selects the [ViewInfo] option for a [Channel]. * @param onBackPressed Handler for back press action. @@ -90,6 +84,7 @@ public fun ChannelsScreen( searchMode: SearchMode = SearchMode.None, onHeaderActionClick: () -> Unit = {}, onHeaderAvatarClick: () -> Unit = {}, + onStartChatClick: (() -> Unit)? = null, onChannelClick: (Channel) -> Unit = {}, onSearchMessageItemClick: (Message) -> Unit = {}, onViewChannelInfoAction: (Channel) -> Unit = {}, @@ -145,7 +140,12 @@ public fun ChannelsScreen( ChatTheme.componentFactory.ChannelListSearchInput( modifier = Modifier .testTag("Stream_SearchInput") - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding( + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingXs, + start = StreamTokens.spacingMd, + end = StreamTokens.spacingMd, + ) .fillMaxWidth(), query = searchQuery, onSearchStarted = {}, @@ -175,16 +175,38 @@ public fun ChannelsScreen( listViewModel.selectChannel(it) } }, + emptyContent = { + ChatTheme.componentFactory.ChannelListEmptyContent( + modifier = Modifier.fillMaxSize(), + onStartChatClick = onStartChatClick, + ) + }, ) } } - val channel = selectedChannel ?: Channel() + val isMenuVisible = selectedChannel != null + val lastChannel = remember { mutableStateOf(Channel()) } + if (selectedChannel != null) { + lastChannel.value = selectedChannel!! + } AnimatedVisibility( - visible = channel.cid.isNotEmpty(), + visible = isMenuVisible, enter = fadeIn(), exit = fadeOut(animationSpec = tween(durationMillis = AnimationConstants.DefaultDurationMillis / 2)), ) { + val channel = lastChannel.value + 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) @@ -200,23 +222,11 @@ public fun ChannelsScreen( ), selectedChannel = channel, currentUser = user, - isMuted = listViewModel.isChannelMuted(channel.cid), - onChannelOptionClick = remember(listViewModel) { + channelActions = channelActions, + onChannelOptionConfirm = 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() } }, @@ -224,32 +234,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/header/ChannelListHeader.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt index 6d0eb6fb5ad..c6f1bc9e3e3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/header/ChannelListHeader.kt @@ -16,21 +16,25 @@ package io.getstream.chat.android.compose.ui.channels.header +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape @@ -42,6 +46,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator +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.clickable @@ -78,7 +83,7 @@ public fun ChannelListHeader( connectionState: ConnectionState, color: Color = ChatTheme.colors.backgroundElevationElevation1, shape: Shape = RectangleShape, - elevation: Dp = StreamTokens.elevation3, + elevation: Dp = 0.dp, onAvatarClick: (User?) -> Unit = {}, onHeaderActionClick: () -> Unit = {}, leadingContent: @Composable RowScope.() -> Unit = { @@ -112,17 +117,24 @@ public fun ChannelListHeader( color = color, shape = shape, ) { - Row( - Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - leadingContent() + Column { + Row( + Modifier + .fillMaxWidth() + .padding(StreamTokens.spacingSm), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + leadingContent() - centerContent() + centerContent() - trailingContent() + trailingContent() + } + HorizontalDivider( + thickness = 1.dp, + color = ChatTheme.colors.borderCoreDefault, + ) } } } @@ -137,19 +149,25 @@ internal fun DefaultChannelHeaderLeadingContent( currentUser: User?, onAvatarClick: (User?) -> Unit, ) { - val size = Modifier.size(40.dp) - if (currentUser != null) { - ChatTheme.componentFactory.UserAvatar( - modifier = size - .clickable { onAvatarClick(currentUser) } - .testTag("Stream_UserAvatar"), - user = currentUser, - showIndicator = false, - showBorder = false, - ) + Box( + modifier = Modifier + .size(AvatarSize.ExtraLarge) + .clip(CircleShape) + .clickable { onAvatarClick(currentUser) }, + contentAlignment = Alignment.Center, + ) { + ChatTheme.componentFactory.UserAvatar( + modifier = Modifier + .size(AvatarSize.Large) + .testTag("Stream_UserAvatar"), + user = currentUser, + showIndicator = false, + showBorder = false, + ) + } } else { - Spacer(modifier = size) + Spacer(modifier = Modifier.size(AvatarSize.ExtraLarge)) } } @@ -171,9 +189,9 @@ internal fun RowScope.DefaultChannelListHeaderCenterContent( modifier = Modifier .weight(1f) .wrapContentWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = StreamTokens.spacingMd), text = title, - style = ChatTheme.typography.headingMedium, + style = ChatTheme.typography.headingSmall, maxLines = 1, color = ChatTheme.colors.textPrimary, ) @@ -185,9 +203,9 @@ internal fun RowScope.DefaultChannelListHeaderCenterContent( modifier = Modifier .weight(1f) .wrapContentWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = StreamTokens.spacingMd), text = stringResource(R.string.stream_compose_disconnected), - style = ChatTheme.typography.headingMedium, + style = ChatTheme.typography.headingSmall, maxLines = 1, color = ChatTheme.colors.textPrimary, ) @@ -209,13 +227,12 @@ internal fun DefaultChannelListHeaderTrailingContent( onClick = onHeaderActionClick, color = ChatTheme.colors.accentPrimary, shape = CircleShape, - shadowElevation = 4.dp, ) { Icon( modifier = Modifier - .wrapContentSize() + .size(20.dp) .testTag("Stream_CreateChannelIcon"), - painter = painterResource(id = R.drawable.stream_compose_ic_new_chat), + painter = painterResource(id = R.drawable.stream_compose_ic_add), contentDescription = stringResource(id = R.string.stream_compose_channel_list_header_new_chat), tint = Color.White, ) 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 870bdb8cbe3..72a8cf0436e 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,33 +33,32 @@ 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 onChannelOptionClick Handler for when the user selects a channel option. + * @param currentUser The currently logged-in user data. + * @param channelActions The list of actions to show in the menu. + * @param onChannelOptionConfirm Handler for when the user selects a channel option. + * Routes through confirmation dialogs for destructive actions before executing. * @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. @@ -65,16 +67,11 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction @Composable public fun SelectedChannelMenu( selectedChannel: Channel, - isMuted: Boolean, currentUser: User?, - onChannelOptionClick: (ChannelAction) -> Unit, + channelActions: List, + onChannelOptionConfirm: (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 = { @@ -90,8 +87,8 @@ public fun SelectedChannelMenu( with(ChatTheme.componentFactory) { ChannelMenuCenterContent( modifier = Modifier, - onChannelOptionClick = onChannelOptionClick, - channelOptions = channelOptions, + onChannelOptionConfirm = onChannelOptionConfirm, + channelActions = channelActions, ) } }, @@ -117,40 +114,53 @@ 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 = 16.dp, end = 16.dp, top = 16.dp), - textAlign = TextAlign.Center, - text = ChatTheme.channelNameFormatter.formatChannelName(selectedChannel, currentUser), - style = ChatTheme.typography.headingMedium, - color = ChatTheme.colors.textPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = selectedChannel.getMembersStatusText( - context = LocalContext.current, + .padding( + start = StreamTokens.spacingMd, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingSm, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + ChatTheme.componentFactory.ChannelAvatar( + modifier = Modifier.size(AvatarSize.ExtraLarge), + channel = selectedChannel, currentUser = currentUser, - userPresence = ChatTheme.userPresence, - ), - style = ChatTheme.typography.metadataEmphasis, - 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, + overflow = TextOverflow.Ellipsis, + ) + } + } } /** @@ -163,6 +173,7 @@ internal fun DefaultSelectedChannelMenuHeaderContent( private fun SelectedChannelMenuCenteredDialogPreview() { ChatTheme { Box(modifier = Modifier.fillMaxSize()) { + val channel = PreviewChannelData.channelWithManyMembers SelectedChannelMenu( modifier = Modifier .padding(16.dp) @@ -170,10 +181,12 @@ private fun SelectedChannelMenuCenteredDialogPreview() { .wrapContentHeight() .align(Alignment.Center), shape = RoundedCornerShape(16.dp), - selectedChannel = PreviewChannelData.channelWithManyMembers, - isMuted = false, + selectedChannel = channel, currentUser = PreviewUserData.user1, - onChannelOptionClick = {}, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), + onChannelOptionConfirm = {}, onDismiss = {}, ) } @@ -190,16 +203,19 @@ 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, - onChannelOptionClick = {}, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), + onChannelOptionConfirm = {}, 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 bf73bb1e313..ab229245713 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 @@ -19,23 +19,31 @@ package io.getstream.chat.android.compose.ui.channels.list import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -46,19 +54,27 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.getstream.chat.android.client.extensions.currentUserUnreadCount import io.getstream.chat.android.client.extensions.getCreatedAtOrNull import io.getstream.chat.android.client.extensions.internal.NEVER +import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.TypingIndicator +import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize +import io.getstream.chat.android.compose.ui.theme.ChannelListConfig +import io.getstream.chat.android.compose.ui.theme.ChatConfig import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition import io.getstream.chat.android.compose.ui.theme.StreamTokens +import io.getstream.chat.android.compose.ui.util.applyIf import io.getstream.chat.android.compose.ui.util.getLastMessage +import io.getstream.chat.android.compose.ui.util.getLastMessageIncludingDeleted +import io.getstream.chat.android.compose.ui.util.isOneToOne import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.DraftMessage +import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus import io.getstream.chat.android.models.User import io.getstream.chat.android.previewdata.PreviewChannelData @@ -117,17 +133,26 @@ public fun ChannelItem( val channel = channelItem.channel val description = stringResource(id = R.string.stream_compose_cd_channel_item) + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + + val shape = RoundedCornerShape(StreamTokens.radiusLg) + Column( modifier = modifier .testTag("Stream_ChannelItem") .fillMaxWidth() .wrapContentHeight() + .padding(horizontal = StreamTokens.spacing2xs) .semantics { contentDescription = description } + .applyIf(isFocused) { border(2.dp, ChatTheme.colors.borderUtilitySelected, shape) } + .clip(shape) + .applyIf(channelItem.isSelected) { background(ChatTheme.colors.backgroundCoreSelected, shape) } .combinedClickable( onClick = { onChannelClick(channel) }, onLongClick = { onChannelLongClick(channel) }, indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, + interactionSource = interactionSource, ), ) { Row( @@ -157,12 +182,12 @@ internal fun DefaultChannelItemLeadingContent( ChatTheme.componentFactory.ChannelAvatar( modifier = Modifier .padding( - start = StreamTokens.spacingXs, - end = 4.dp, - top = StreamTokens.spacingSm, - bottom = StreamTokens.spacingSm, + start = StreamTokens.spacingMd, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingMd, ) - .size(40.dp), + .size(AvatarSize.ExtraLarge), channel = channelItem.channel, currentUser = currentUser, showIndicator = false, @@ -182,65 +207,165 @@ internal fun RowScope.DefaultChannelItemCenterContent( channelItemState: ItemState.ChannelItemState, currentUser: User?, ) { + val channel = channelItemState.channel + val isDirectMessaging = channel.isOneToOne(currentUser) + // Use raw last message (including deleted) for preview; fall back to filtered for non-deleted + val rawLastMessage = channel.getLastMessageIncludingDeleted(currentUser) + val isLastMessageDeleted = rawLastMessage?.isDeleted() == true + val lastMessage = if (isLastMessageDeleted) rawLastMessage else channel.getLastMessage(currentUser) + val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) + val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id + + val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + Column( modifier = Modifier - .padding(start = 4.dp, end = 4.dp) .weight(1f) - .wrapContentHeight(), - verticalArrangement = Arrangement.Center, + .padding(vertical = StreamTokens.spacing3xs), + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), ) { - val channelName: (@Composable (modifier: Modifier) -> Unit) = @Composable { + TitleRow( + channelItemState = channelItemState, + currentUser = currentUser, + lastMessage = lastMessage, + unreadCount = unreadCount, + ) + + MessageRow( + channelItemState = channelItemState, + currentUser = currentUser, + lastMessage = lastMessage, + isLastMessageDeleted = isLastMessageDeleted, + ) + } +} + +@Composable +private fun TitleRow( + channelItemState: ItemState.ChannelItemState, + currentUser: User?, + lastMessage: Message?, + unreadCount: Int, +) { + val channel = channelItemState.channel + val isMuted = channelItemState.isMuted + val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + ) { Text( - modifier = it.testTag("Stream_ChannelName"), - text = ChatTheme.channelNameFormatter.formatChannelName(channelItemState.channel, currentUser), - style = ChatTheme.typography.bodyEmphasis, - fontSize = 16.sp, + modifier = Modifier + .testTag("Stream_ChannelName") + .weight(1f, fill = false), + text = ChatTheme.channelNameFormatter.formatChannelName(channel, currentUser), + style = ChatTheme.typography.headingSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, color = ChatTheme.colors.textPrimary, ) - } - - if (channelItemState.isMuted) { - Row(verticalAlignment = Alignment.CenterVertically) { - channelName(Modifier.weight(weight = 1f, fill = false)) + if (isMuted && mutePosition == MuteIndicatorPosition.INLINE_TITLE) { Icon( modifier = Modifier .testTag("Stream_ChannelMutedIcon") - .padding(start = 8.dp) .size(16.dp), painter = painterResource(id = R.drawable.stream_compose_ic_muted), contentDescription = null, - tint = ChatTheme.colors.textSecondary, + tint = ChatTheme.colors.textTertiary, ) } - } else { - channelName(Modifier) } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs), + ) { + if (lastMessage != null) { + Timestamp( + date = lastMessage.getCreatedAtOrNull(), + textStyle = ChatTheme.typography.captionDefault.copy( + color = ChatTheme.colors.textTertiary, + ), + ) + } + + if (unreadCount > 0) { + ChatTheme.componentFactory.ChannelItemUnreadCountIndicator( + unreadCount = unreadCount, + modifier = Modifier, + ) + } + } + } +} + +@Composable +private fun MessageRow( + channelItemState: ItemState.ChannelItemState, + currentUser: User?, + lastMessage: Message?, + isLastMessageDeleted: Boolean, +) { + val channel = channelItemState.channel + val isDirectMessaging = channel.isOneToOne(currentUser) + val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id + val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + ) { if (channelItemState.typingUsers.isNotEmpty()) { - UserTypingIndicator(channelItemState.typingUsers) + UserTypingIndicator(channelItemState.typingUsers, isDirectMessaging) } else { + if (isLastMessageFromCurrentUser && lastMessage != null && !isLastMessageDeleted) { + ChatTheme.componentFactory.ChannelItemReadStatusIndicator( + channel = channel, + message = lastMessage, + currentUser = currentUser, + modifier = Modifier, + ) + } + val lastMessageText = channelItemState.draftMessage ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } - ?: channelItemState.channel.getLastMessage(currentUser)?.let { lastMessage -> - ChatTheme.messagePreviewFormatter.formatMessagePreview(lastMessage, currentUser) + ?: lastMessage?.let { + ChatTheme.messagePreviewFormatter.formatMessagePreview( + it, currentUser, isDirectMessaging, + ) } - ?: AnnotatedString("") - - if (lastMessageText.isNotEmpty()) { - Text( - modifier = Modifier.testTag("Stream_MessagePreview"), - text = lastMessageText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.bodyDefault, - color = ChatTheme.colors.textSecondary, - inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), - ) - } + + Text( + modifier = Modifier + .testTag("Stream_MessagePreview") + .weight(1f), + text = lastMessageText + ?: AnnotatedString(stringResource(R.string.stream_compose_no_messages_yet)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, + inlineContent = ChatTheme.messagePreviewIconFactory.createPreviewIcons(), + ) + } + + if (channelItemState.isMuted && mutePosition == MuteIndicatorPosition.TRAILING_BOTTOM) { + Icon( + modifier = Modifier + .testTag("Stream_ChannelMutedIcon") + .size(16.dp), + painter = painterResource(id = R.drawable.stream_compose_ic_muted), + contentDescription = null, + tint = ChatTheme.colors.textTertiary, + ) } } } @@ -251,24 +376,34 @@ internal fun RowScope.DefaultChannelItemCenterContent( * @param users The list of users currently typing. */ @Composable -private fun UserTypingIndicator(users: List) { +private fun UserTypingIndicator(users: List, isDirectMessaging: Boolean) { + val context = LocalContext.current + val typingText = when { + isDirectMessaging -> stringResource(R.string.stream_compose_channel_list_typing) + users.size == 1 -> stringResource(R.string.stream_compose_channel_list_typing_one, users.first().name) + users.size == 2 -> stringResource( + R.string.stream_compose_channel_list_typing_two, + users[0].name, + users[1].name, + ) + else -> context.resources.getQuantityString( + R.plurals.stream_compose_channel_list_typing_many, + users.size, + users.size, + ) + } Row( modifier = Modifier, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), // 4dp (was 6dp) verticalAlignment = Alignment.CenterVertically, ) { TypingIndicator() Text( modifier = Modifier.testTag("Stream_ChannelListTypingIndicator"), - text = LocalContext.current.resources.getQuantityString( - R.plurals.stream_compose_message_list_header_typing_users, - users.size, - users.first().name, - users.size - 1, - ), + text = typingText, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.bodyDefault, + style = ChatTheme.typography.captionDefault, color = ChatTheme.colors.textSecondary, ) } @@ -287,50 +422,9 @@ internal fun RowScope.DefaultChannelItemTrailingContent( channel: Channel, currentUser: User?, ) { - val lastMessage = channel.getLastMessage(currentUser) - - if (lastMessage != null) { - Column( - modifier = Modifier - .padding( - start = 4.dp, - end = StreamTokens.spacingXs, - top = StreamTokens.spacingSm, - bottom = StreamTokens.spacingSm, - ) - .wrapContentHeight() - .align(Alignment.Bottom), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - val unreadCount = channel.currentUserUnreadCount(currentUserId = currentUser?.id) - - if (unreadCount > 0) { - ChatTheme.componentFactory.ChannelItemUnreadCountIndicator( - unreadCount = unreadCount, - modifier = Modifier, - ) - } - - val isLastMessageFromCurrentUser = lastMessage.user.id == currentUser?.id - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - if (isLastMessageFromCurrentUser) { - ChatTheme.componentFactory.ChannelItemReadStatusIndicator( - channel = channel, - message = lastMessage, - currentUser = currentUser, - modifier = Modifier, - ) - } - - Timestamp(date = lastMessage.getCreatedAtOrNull()) - } - } - } + // Timestamp, badge, and delivery status have moved to DefaultChannelItemCenterContent. + // This slot remains for API compatibility and provides the trailing end padding. + Spacer(modifier = Modifier.width(StreamTokens.spacingMd)) } @Preview(showBackground = true) @@ -366,6 +460,27 @@ internal fun ChannelItemMuted() { ) } +@Preview(showBackground = true) +@Composable +private fun ChannelItemMutedTrailingBottomPreview() { + ChatTheme( + config = ChatConfig( + channelList = ChannelListConfig(muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM), + ), + ) { + ChannelItemMutedTrailingBottom() + } +} + +@Composable +internal fun ChannelItemMutedTrailingBottom() { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isMuted = true, + ) +} + @Preview(showBackground = true) @Composable private fun ChannelItemUnreadMessagesPreview() { @@ -494,15 +609,34 @@ private fun ChannelItem( channel: Channel, isMuted: Boolean = false, draftMessage: DraftMessage? = null, + isSelected: Boolean = false, ) { ChannelItem( channelItem = ItemState.ChannelItemState( channel = channel, isMuted = isMuted, draftMessage = draftMessage, + isSelected = isSelected, ), currentUser = currentUser, onChannelClick = {}, onChannelLongClick = {}, ) } + +@Preview(showBackground = true) +@Composable +private fun ChannelItemSelectedPreview() { + ChatTheme { + ChannelItemSelected() + } +} + +@Composable +internal fun ChannelItemSelected() { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isSelected = true, + ) +} 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 5bfba4205a2..e346c37dd9b 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 @@ -16,19 +16,32 @@ package io.getstream.chat.android.compose.ui.channels.list +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.material3.Text 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.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -37,10 +50,12 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.channels.list.ChannelsState import io.getstream.chat.android.compose.state.channels.list.ItemState import io.getstream.chat.android.compose.ui.components.EmptyContent -import io.getstream.chat.android.compose.ui.components.LoadingIndicator import io.getstream.chat.android.compose.ui.components.StreamHorizontalDivider +import io.getstream.chat.android.compose.ui.components.button.StreamButtonStyleDefaults +import io.getstream.chat.android.compose.ui.components.button.StreamTextButton import io.getstream.chat.android.compose.ui.theme.ChatPreviewTheme 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 @@ -50,6 +65,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 @@ -98,7 +121,7 @@ public fun ChannelList( ChatTheme.componentFactory.ChannelListLoadingIndicator(modifier = modifier) }, emptyContent: @Composable () -> Unit = { - ChatTheme.componentFactory.ChannelListEmptyContent(modifier = modifier) + ChatTheme.componentFactory.ChannelListEmptyContent(modifier = modifier, onStartChatClick = null) }, emptySearchContent: @Composable (String) -> Unit = { searchQuery -> ChatTheme.componentFactory.ChannelListEmptySearchContent( @@ -118,9 +141,15 @@ public fun ChannelList( }, channelContent: @Composable LazyItemScope.(ItemState.ChannelItemState) -> Unit = { itemState -> val user by viewModel.user.collectAsState() + val selectedCid = viewModel.selectedChannel.value?.cid + val enrichedItemState = if (selectedCid != null && itemState.channel.cid == selectedCid) { + itemState.copy(isSelected = true) + } else { + itemState + } with(ChatTheme.componentFactory) { ChannelListItemContent( - channelItem = itemState, + channelItem = enrichedItemState, currentUser = user, onChannelClick = onChannelClick, onChannelLongClick = onChannelLongClick, @@ -144,25 +173,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, + ) + } } /** @@ -216,7 +275,7 @@ public fun ChannelList( ChatTheme.componentFactory.ChannelListLoadingIndicator(modifier = modifier) }, emptyContent: @Composable () -> Unit = { - ChatTheme.componentFactory.ChannelListEmptyContent(modifier = modifier) + ChatTheme.componentFactory.ChannelListEmptyContent(modifier = modifier, onStartChatClick = null) }, emptySearchContent: @Composable (String) -> Unit = { searchQuery -> ChatTheme.componentFactory.ChannelListEmptySearchContent( @@ -308,27 +367,60 @@ internal fun LazyItemScope.WrapperItemContent( } /** - * Default loading indicator. + * Default loading indicator showing skeleton shimmer items. * * @param modifier Modifier for styling. */ @Composable internal fun DefaultChannelListLoadingIndicator(modifier: Modifier) { - LoadingIndicator(modifier) + LazyColumn( + modifier = modifier + .testTag("Stream_ChannelListLoading") + .background(ChatTheme.colors.backgroundCoreApp), + userScrollEnabled = false, + ) { + items(count = 8) { ChannelListLoadingItem() } + } } /** * The default empty placeholder for the case when there are no channels available to the user. * * @param modifier Modifier for styling. + * @param onStartChatClick Optional callback for the "Start a chat" button. If null, the button is hidden. */ @Composable -internal fun DefaultChannelListEmptyContent(modifier: Modifier = Modifier) { - EmptyContent( - modifier = modifier, - painter = painterResource(id = R.drawable.stream_compose_empty_channels), - text = stringResource(R.string.stream_compose_channel_list_empty_channels), - ) +internal fun DefaultChannelListEmptyContent( + modifier: Modifier = Modifier, + onStartChatClick: (() -> Unit)? = null, +) { + Column( + modifier = modifier.background(color = ChatTheme.colors.backgroundCoreApp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_empty_channels), + contentDescription = null, + tint = ChatTheme.colors.textTertiary, + modifier = Modifier.size(StreamTokens.spacing2xl), + ) + Spacer(Modifier.size(StreamTokens.spacingXs)) + Text( + text = stringResource(R.string.stream_compose_channel_list_empty_channels), + style = ChatTheme.typography.captionDefault, + color = ChatTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + if (onStartChatClick != null) { + Spacer(Modifier.size(StreamTokens.spacingMd)) + StreamTextButton( + onClick = onStartChatClick, + text = stringResource(R.string.stream_compose_channel_list_start_chat), + style = StreamButtonStyleDefaults.secondaryOutline, + ) + } + } } /** diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelListLoadingItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelListLoadingItem.kt new file mode 100644 index 00000000000..9e27df9c361 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelListLoadingItem.kt @@ -0,0 +1,90 @@ +/* + * 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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.components.ShimmerProgressIndicator + +/** + * A skeleton shimmer placeholder item that mirrors the channel item layout. + * Used as loading content while channels are being fetched. + */ +@Composable +internal fun ChannelListLoadingItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Avatar placeholder + ShimmerProgressIndicator( + modifier = Modifier + .size(48.dp) + .clip(CircleShape), + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier.weight(1f), + ) { + // Title + timestamp row + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + ShimmerProgressIndicator( + modifier = Modifier + .weight(1f) + .height(16.dp) + .clip(RoundedCornerShape(4.dp)), + ) + Spacer(modifier = Modifier.width(16.dp)) + ShimmerProgressIndicator( + modifier = Modifier + .width(48.dp) + .height(16.dp) + .clip(RoundedCornerShape(4.dp)), + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Message preview placeholder + ShimmerProgressIndicator( + modifier = Modifier + .width(200.dp) + .height(16.dp) + .clip(RoundedCornerShape(4.dp)), + ) + } + } +} 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..dcd9256417b --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt @@ -0,0 +1,166 @@ +/* + * 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 android.content.res.Resources +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +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.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 DefaultChannelSwipeActions(channelItem: ItemState.ChannelItemState) { + val handler = LocalSwipeActionHandler.current ?: return + val coordinator = LocalSwipeRevealCoordinator.current + val moreHandler = LocalChannelMoreClickHandler.current + val scope = rememberCoroutineScope() + val channel = channelItem.channel + + if (moreHandler != null) { + SwipeActionItem( + icon = painterResource(R.drawable.stream_compose_ic_more_options), + label = LocalContext.current.resources.getString(R.string.stream_compose_swipe_action_more), + onClick = { + scope.launch { coordinator?.closeAll() } + moreHandler(channel) + }, + style = SwipeActionStyle.Secondary, + ) + } + + val primaryAction = rememberPrimarySwipeAction( + channel = channel, + isMuted = channelItem.isMuted, + handler = handler, + ) + if (primaryAction != null) { + SwipeActionItem( + icon = painterResource(primaryAction.icon), + label = primaryAction.label, + onClick = { primaryAction.onAction() }, + style = SwipeActionStyle.Primary, + ) + } +} + +/** + * Resolves and remembers 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 rememberPrimarySwipeAction( + channel: Channel, + isMuted: Boolean, + handler: (ChannelAction) -> Unit, +): ChannelAction? { + val resources = LocalContext.current.resources + val handlerState = rememberUpdatedState(handler) + val isPinned = channel.isPinned() + val isArchived = channel.isArchive() + val canMute = channel.ownCapabilities.contains(ChannelCapabilities.MUTE_CHANNEL) + val isDM = channel.isDistinct() && channel.members.size == 2 + + return remember(channel.cid, isMuted, isPinned, isArchived, canMute, isDM) { + var resolved: ChannelAction? = null + val onAction: () -> Unit = { resolved?.let { handlerState.value(it) } } + + val archiveAction = archiveAction(channel, isArchived, resources, onAction) + val muteAction = muteAction(channel, isMuted, canMute, resources, onAction) + val pinAction = pinAction(channel, isPinned, resources, onAction) + + val candidates: List = if (isDM) { + listOf(archiveAction, muteAction, pinAction) + } else { + listOf(muteAction, archiveAction, pinAction) + } + + resolved = candidates.firstOrNull { it != null } + resolved + } +} + +private fun archiveAction( + channel: Channel, + isArchived: Boolean, + resources: Resources, + onAction: () -> Unit, +): ChannelAction = if (isArchived) { + UnarchiveChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unarchive), onAction) +} else { + ArchiveChannel(channel, resources.getString(R.string.stream_compose_swipe_action_archive), onAction) +} + +private fun muteAction( + channel: Channel, + isMuted: Boolean, + canMute: Boolean, + resources: Resources, + onAction: () -> Unit, +): ChannelAction? = when { + !canMute -> null + isMuted -> UnmuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unmute), onAction) + else -> MuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_mute), onAction) +} + +private fun pinAction( + channel: Channel, + isPinned: Boolean, + resources: Resources, + onAction: () -> Unit, +): ChannelAction = if (isPinned) { + UnpinChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unpin), onAction) +} else { + PinChannel(channel, resources.getString(R.string.stream_compose_swipe_action_pin), onAction) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt index dc65f943a0b..a8c30596057 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SearchResultItem.kt @@ -90,6 +90,7 @@ public fun SearchResultItem( Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingMd), ) { leadingContent(searchResultItemState) centerContent(searchResultItemState) @@ -122,12 +123,12 @@ internal fun DefaultSearchResultItemLeadingContent( user = user, modifier = Modifier .padding( - start = StreamTokens.spacingXs, - end = 4.dp, - top = StreamTokens.spacingSm, - bottom = StreamTokens.spacingSm, + start = StreamTokens.spacingMd, + end = 0.dp, + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingMd, ) - .size(40.dp), + .size(48.dp), showIndicator = user.shouldShowOnlineIndicator( userPresence = ChatTheme.userPresence, currentUser = currentUser, @@ -151,15 +152,19 @@ internal fun RowScope.DefaultSearchResultItemCenterContent( ) { Column( modifier = Modifier - .padding(start = 4.dp, end = 4.dp) + .padding( + start = StreamTokens.spacing2xs, + end = StreamTokens.spacing2xs, + top = StreamTokens.spacing3xs, + bottom = StreamTokens.spacing3xs, + ) .weight(1f) .wrapContentHeight(), - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), ) { Text( text = ChatTheme.searchResultNameFormatter.formatMessageTitle(searchResultItemState, currentUser), - style = ChatTheme.typography.bodyEmphasis, - fontSize = 16.sp, + style = ChatTheme.typography.bodyDefault, maxLines = 1, overflow = TextOverflow.Ellipsis, color = ChatTheme.colors.textPrimary, @@ -169,10 +174,11 @@ internal fun RowScope.DefaultSearchResultItemCenterContent( text = ChatTheme.messagePreviewFormatter.formatMessagePreview( searchResultItemState.message, currentUser, + false, ), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = ChatTheme.typography.bodyDefault, + style = ChatTheme.typography.captionDefault, color = ChatTheme.colors.textSecondary, ) } @@ -190,17 +196,21 @@ internal fun RowScope.DefaultSearchResultItemTrailingContent( Column( modifier = Modifier .padding( - start = 4.dp, - end = StreamTokens.spacingXs, - top = StreamTokens.spacingSm, - bottom = StreamTokens.spacingSm, + start = StreamTokens.spacing2xs, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingMd, + bottom = StreamTokens.spacingMd, ) .wrapContentHeight() - .align(Alignment.Bottom), + .align(Alignment.CenterVertically), horizontalAlignment = Alignment.End, ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Timestamp(date = searchResultItemState.message.createdAt) - } + Timestamp( + date = searchResultItemState.message.createdAt, + textStyle = ChatTheme.typography.captionDefault.copy( + color = ChatTheme.colors.textTertiary, + lineHeight = 20.sp, + ), + ) } } 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..a87dfede1ca --- /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 Accessibility label used as the icon's content description. + * @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 Accessibility label used as the icon's content description. + * @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 colors.buttonPrimaryTextOnAccent + SwipeActionStyle.Secondary -> colors.backgroundCoreSurface to colors.textPrimary + SwipeActionStyle.Destructive -> colors.accentError to colors.buttonDestructiveTextOnAccent + } + 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..6c0f4b11594 --- /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.toMap().forEach { (key, state) -> + if (key != cid && state.currentValue == SwipeRevealValue.Open) { + state.animateTo(SwipeRevealValue.Closed) + } + } + } + + /** + * Closes all currently open items. + */ + public suspend fun closeAll() { + registry.toMap().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..c35be251f8a --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/SwipeableChannelItem.kt @@ -0,0 +1,141 @@ +/* + * 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.ExperimentalFoundationApi +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. + */ +@OptIn(ExperimentalFoundationApi::class) +@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/SearchInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt index 58720233d83..70f9126e101 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/SearchInput.kt @@ -16,18 +16,24 @@ package io.getstream.chat.android.compose.ui.components +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,14 +41,20 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.ui.components.composer.InputField 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.padding /** * The search component that allows the user to fill in a search query and filter their items. @@ -59,6 +71,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme * @param label The label shown in the search component, when there's no input. * @param clearButton The clear button shown when the search is focused and not empty. */ +@Suppress("LongMethod") @Composable public fun SearchInput( query: String, @@ -87,8 +100,23 @@ public fun SearchInput( null } - InputField( + var textState by remember { mutableStateOf(TextFieldValue(text = query)) } + if (textState.text != query) { + LaunchedEffect(query) { + if (textState.text != query) { + textState = textState.copy( + text = query, + selection = TextRange(query.length), + ) + } + } + } + + val shape = RoundedCornerShape(StreamTokens.radius3xl) + + BasicTextField( modifier = modifier + .defaultMinSize(minHeight = 48.dp) .onFocusEvent { newState -> val wasPreviouslyFocused = isFocused @@ -97,12 +125,36 @@ public fun SearchInput( } isFocused = newState.isFocused - }, - value = query, - onValueChange = onValueChange, + } + .border( + border = BorderStroke(1.dp, ChatTheme.colors.borderCoreDefault), + shape = shape, + ) + .clip(shape), + value = textState, + onValueChange = { + textState = it + if (query != it.text) { + onValueChange(it.text) + } + }, + textStyle = ChatTheme.typography.bodyDefault.copy( + color = ChatTheme.colors.textPrimary, + ), + cursorBrush = SolidColor(ChatTheme.colors.accentPrimary), + singleLine = true, + maxLines = 1, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), decorationBox = { innerTextField -> Row( - Modifier.fillMaxWidth(), + Modifier + .fillMaxWidth() + .padding( + start = StreamTokens.spacingXs, + end = StreamTokens.spacingMd, + top = StreamTokens.spacingSm, + bottom = StreamTokens.spacingSm, + ), verticalAlignment = Alignment.CenterVertically, ) { leadingIcon() @@ -118,8 +170,6 @@ public fun SearchInput( trailingContent?.invoke(this) } }, - maxLines = 1, - innerPadding = PaddingValues(4.dp), ) } @@ -129,7 +179,12 @@ public fun SearchInput( @Composable internal fun DefaultSearchLeadingIcon() { Icon( - modifier = Modifier.padding(horizontal = 6.dp), + modifier = Modifier + .padding( + start = StreamTokens.spacingSm, + end = StreamTokens.spacingXs, + ) + .size(16.dp), painter = painterResource(id = R.drawable.stream_compose_ic_search), contentDescription = null, tint = ChatTheme.colors.textTertiary, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt index efe6300d637..e92141a98aa 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/StreamHorizontalDivider.kt @@ -26,6 +26,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme internal fun StreamHorizontalDivider(thickness: Dp = .5.dp) { HorizontalDivider( thickness = thickness, - color = ChatTheme.colors.borderCoreDefault, + color = ChatTheme.colors.borderCoreSubtle, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt index e3b0a74f6c8..2efd46ad7da 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/Timestamp.kt @@ -27,6 +27,7 @@ import io.getstream.chat.android.compose.state.DateFormatType.DATE import io.getstream.chat.android.compose.state.DateFormatType.RELATIVE import io.getstream.chat.android.compose.state.DateFormatType.TIME import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.ui.common.helper.DateFormatter import java.util.Date @@ -45,7 +46,10 @@ public fun Timestamp( modifier: Modifier = Modifier, formatter: DateFormatter = ChatTheme.dateFormatter, formatType: DateFormatType = DATE, - textStyle: TextStyle = ChatTheme.typography.metadataDefault.copy(ChatTheme.colors.chatTextTimestamp), + textStyle: TextStyle = ChatTheme.typography.captionDefault.copy( + color = ChatTheme.colors.textTertiary, + lineHeight = StreamTokens.lineHeightNormal, + ), ) { val timestamp = if (LocalInspectionMode.current) { "13:49" 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..f1110de8f41 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,29 @@ 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.runtime.collectAsState +import androidx.compose.runtime.getValue 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,29 +53,29 @@ 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 onChannelOptionClick Handler for when the user selects a channel action. + * @param actions The list of channel actions to show in the UI. + * @param onChannelOptionConfirm Handler for when the user selects a channel action. + * Routes through confirmation dialogs for destructive actions before executing. * @param modifier Modifier for styling. */ @Composable public fun ChannelOptions( - options: List, - onChannelOptionClick: (ChannelAction) -> Unit, + actions: List, + onChannelOptionConfirm: (ChannelAction) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( 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 = { onChannelOptionConfirm(action) }, ) } } @@ -82,177 +83,178 @@ 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 currentUser by viewModel.user.collectAsState() + val channelName = ChatTheme.channelNameFormatter.formatChannelName( + selectedChannel, + currentUser, + ) 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,13 +268,26 @@ 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 = {}, + onChannelOptionConfirm = {}, ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt index 07c8e2cb2d0..80b8ea54104 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessageReadStatusIcon.kt @@ -105,7 +105,7 @@ public fun MessageReadStatusIcon( else -> isSentIcon() } - SyncStatus.FAILED_PERMANENTLY -> Unit + SyncStatus.FAILED_PERMANENTLY -> IsErrorIcon(modifier = modifier) } } @@ -139,7 +139,7 @@ private fun IsReadCount( modifier = Modifier.testTag("Stream_MessageReadStatus_isRead"), painter = painterResource(id = R.drawable.stream_compose_message_seen), contentDescription = null, - tint = ChatTheme.colors.accentPrimary, + tint = ChatTheme.colors.chatTextRead, ) } } @@ -164,6 +164,16 @@ private fun IsSentIcon(modifier: Modifier) { ) } +@Composable +private fun IsErrorIcon(modifier: Modifier) { + Icon( + modifier = modifier.testTag("Stream_MessageReadStatus_isError"), + painter = painterResource(id = R.drawable.stream_compose_ic_error), + contentDescription = stringResource(R.string.stream_ui_message_list_semantics_message_status_failed), + tint = ChatTheme.colors.accentError, + ) +} + @Composable private fun IsDeliveredIcon(modifier: Modifier) { Icon( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt index 54e120c6cbf..f74faba21fd 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt @@ -20,7 +20,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens /** * Shows the unread count badge for each channel item, to showcase how many messages @@ -43,22 +44,21 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme public fun UnreadCountIndicator( unreadCount: Int, modifier: Modifier = Modifier, - color: Color = ChatTheme.colors.accentError, + color: Color = ChatTheme.colors.accentPrimary, ) { val displayText = if (unreadCount > LimitTooManyUnreadCount) UnreadCountMany else unreadCount.toString() - val shape = RoundedCornerShape(9.dp) Box( modifier = modifier - .defaultMinSize(minWidth = 18.dp, minHeight = 18.dp) - .background(shape = shape, color = color) - .padding(horizontal = 4.dp), + .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) + .background(shape = CircleShape, color = color) + .padding(horizontal = StreamTokens.spacing2xs), // 4dp horizontal content padding contentAlignment = Alignment.Center, ) { Text( modifier = Modifier.testTag("Stream_UnreadCountIndicator"), text = displayText, - color = Color.White, + color = ChatTheme.colors.badgeTextOnAccent, textAlign = TextAlign.Center, style = ChatTheme.typography.numericMedium, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/preview/internal/MessagePreviewItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/preview/internal/MessagePreviewItem.kt index 1aeac461475..ea994b93ef3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/preview/internal/MessagePreviewItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/preview/internal/MessagePreviewItem.kt @@ -151,7 +151,7 @@ internal fun RowScope.DefaultMessagePreviewItemCenterContent( ) Text( - text = ChatTheme.messagePreviewFormatter.formatMessagePreview(message, currentUser), + text = ChatTheme.messagePreviewFormatter.formatMessagePreview(message, currentUser, false), maxLines = 1, overflow = TextOverflow.Ellipsis, style = ChatTheme.typography.bodyDefault, 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 ea8d21ff537..1ee2ed9624a 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 @@ -69,7 +69,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 @@ -98,11 +97,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 @@ -384,16 +386,25 @@ public interface ChatComponentFactory { /** * The default empty content of the channel list. + * + * @param modifier Modifier for styling. + * @param onStartChatClick Optional callback for the "Start a chat" button. */ @Composable - public fun ChannelListEmptyContent(modifier: Modifier) { + public fun ChannelListEmptyContent( + modifier: Modifier, + onStartChatClick: (() -> Unit)?, + ) { DefaultChannelListEmptyContent( modifier = modifier, + onStartChatClick = onStartChatClick, ) } /** * 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( @@ -402,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( + modifier = Modifier.animateItem(), + channelCid = channelItem.channel.cid, + backgroundColor = ChatTheme.colors.backgroundCoreApp, + swipeActions = { ChannelSwipeActions(channelItem) }, + ) { + ChannelItem( + 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 ChannelSwipeActions(channelItem: ItemState.ChannelItemState) { + DefaultChannelSwipeActions(channelItem) } /** @@ -2056,26 +2097,27 @@ 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 onChannelOptionConfirm Callback for confirming a channel action. + * Routes through confirmation dialogs for destructive actions. * @param onDismiss Callback for when the menu is dismissed. */ @Composable public fun ChannelMenu( modifier: Modifier, selectedChannel: Channel, - isMuted: Boolean, currentUser: User?, - onChannelOptionClick: (ChannelAction) -> Unit, + channelActions: List, + onChannelOptionConfirm: (ChannelAction) -> Unit, onDismiss: () -> Unit, ) { SelectedChannelMenu( modifier = modifier, selectedChannel = selectedChannel, - isMuted = isMuted, currentUser = currentUser, - onChannelOptionClick = onChannelOptionClick, + channelActions = channelActions, + onChannelOptionConfirm = onChannelOptionConfirm, onDismiss = onDismiss, ) } @@ -2101,18 +2143,19 @@ 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 onChannelOptionConfirm Callback for confirming a channel action. + * Routes through confirmation dialogs for destructive actions. + * @param channelActions List of channel actions. */ @Composable public fun ChannelMenuCenterContent( modifier: Modifier, - onChannelOptionClick: (ChannelAction) -> Unit, - channelOptions: List, + onChannelOptionConfirm: (ChannelAction) -> Unit, + channelActions: List, ) { ChannelMenuOptions( - channelOptions = channelOptions, - onChannelOptionClick = onChannelOptionClick, + channelActions = channelActions, + onChannelOptionConfirm = onChannelOptionConfirm, modifier = modifier, ) } @@ -2120,43 +2163,51 @@ 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 onChannelOptionConfirm Callback for confirming a channel action. + * Routes through confirmation dialogs for destructive actions. + * @param channelActions List of channel actions. */ @Composable public fun ChannelMenuOptions( modifier: Modifier, - onChannelOptionClick: (ChannelAction) -> Unit, - channelOptions: List, + onChannelOptionConfirm: (ChannelAction) -> Unit, + channelActions: List, ) { ChannelOptions( - options = channelOptions, - onChannelOptionClick = onChannelOptionClick, + actions = channelActions, + onChannelOptionConfirm = onChannelOptionConfirm, 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.bodyEmphasis, - itemHeight = 56.dp, + style = ChatTheme.typography.bodyDefault, + itemHeight = 44.dp, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) @@ -2165,16 +2216,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 3bb0e2ca4f7..a15a13a3d35 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 @@ -25,6 +25,28 @@ import io.getstream.chat.android.compose.state.messages.attachments.GalleryPicke import io.getstream.chat.android.compose.state.messages.attachments.PollPickerMode import kotlinx.parcelize.Parcelize +/** + * Defines where the mute indicator icon is placed in the channel list item. + */ +public enum class MuteIndicatorPosition { + /** Icon appears inline after the channel name in the title row. */ + INLINE_TITLE, + + /** Icon appears at the trailing end of the message/preview row. */ + TRAILING_BOTTOM, +} + +/** + * 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, +) + /** * Central behavioral configuration for the Chat SDK, accessible through `ChatTheme.config`. * @@ -36,6 +58,7 @@ import kotlinx.parcelize.Parcelize * @param messageList Configuration for the message list behavior. * @param mediaGallery Configuration for the media gallery preview screen. * @param composer Configuration for the message composer behavior. + * @param channelList Configuration for the channel list behavior. * @param attachmentPicker Configuration for the attachment picker behavior. */ public data class ChatConfig( @@ -43,6 +66,7 @@ public data class ChatConfig( val messageList: MessageListConfig = MessageListConfig(), val mediaGallery: MediaGalleryConfig = MediaGalleryConfig(), val composer: ComposerConfig = ComposerConfig(), + val channelList: ChannelListConfig = ChannelListConfig(), val attachmentPicker: AttachmentPickerConfig = AttachmentPickerConfig(), ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt index aaaef026b35..e9743ad1963 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt @@ -38,6 +38,7 @@ internal object StreamTokens { val radiusFull = CornerSize(percent = 50) val size12 = 12.dp + val size16 = 16.dp val spacing3xs = 2.dp val spacing2xs = 4.dp diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt index c30af474410..b5c40038d7c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadItem.kt @@ -268,7 +268,7 @@ private fun formatMessage(message: Message) = } } } else { - ChatTheme.messagePreviewFormatter.formatMessagePreview(message, null) + ChatTheme.messagePreviewFormatter.formatMessagePreview(message, null, false) } @Composable diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt index ebc8b909ac3..30c29a50b1a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ChannelUtils.kt @@ -17,6 +17,11 @@ package io.getstream.chat.android.compose.ui.util import android.content.Context +import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault +import io.getstream.chat.android.client.extensions.internal.NEVER +import io.getstream.chat.android.client.utils.message.isDeleted +import io.getstream.chat.android.client.utils.message.isRegular +import io.getstream.chat.android.client.utils.message.isSystem import io.getstream.chat.android.compose.R import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Message @@ -34,6 +39,17 @@ import java.util.Date */ public fun Channel.getLastMessage(currentUser: User?): Message? = getPreviewMessage(currentUser) +/** + * Returns channel's last regular or system message, **including deleted messages**. + * Used by the channel list to show "Message deleted" when the last message was deleted. + */ +internal fun Channel.getLastMessageIncludingDeleted(currentUser: User?): Message? = + messages.asSequence() + .filter { it.createdAt != null || it.createdLocallyAt != null } + .filter { it.user.id == currentUser?.id || !it.shadowed } + .filter { it.isRegular() || it.isSystem() || it.isDeleted() } + .maxByOrNull { it.getCreatedAtOrDefault(NEVER) } + /** * Filters the read status of each person other than the target user. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index 8ed0bd00943..980b0c9accb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import io.getstream.chat.android.client.utils.message.hasAudioRecording import io.getstream.chat.android.client.utils.message.hasSharedLocation +import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isPoll import io.getstream.chat.android.client.utils.message.isPollClosed import io.getstream.chat.android.client.utils.message.isSystem @@ -34,6 +35,7 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.theme.StreamDesign import io.getstream.chat.android.models.Attachment +import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User @@ -56,9 +58,15 @@ public interface MessagePreviewFormatter { * * @param message The message whose data is used to generate the preview text. * @param currentUser The currently logged in user. + * @param isDirectMessaging Whether the channel is a direct message conversation. + * Used to determine sender prefix behavior. * @return The formatted text representation for the given message. */ - public fun formatMessagePreview(message: Message, currentUser: User?): AnnotatedString + public fun formatMessagePreview( + message: Message, + currentUser: User?, + isDirectMessaging: Boolean, + ): AnnotatedString /** * Generates a preview text for the given draft message. @@ -154,23 +162,48 @@ private class DefaultMessagePreviewFormatter( * @param currentUser The currently logged in user. * @return The formatted text representation for the given message. */ + @Suppress("LongMethod") override fun formatMessagePreview( message: Message, currentUser: User?, + isDirectMessaging: Boolean, ): AnnotatedString { return buildAnnotatedString { - message.let { message -> - val displayedText = when (autoTranslationEnabled) { - true -> currentUser?.language?.let { userLanguage -> - message.getTranslation(userLanguage).ifEmpty { message.text } - } ?: message.text + val displayedText = when (autoTranslationEnabled) { + true -> currentUser?.language?.let { userLanguage -> + message.getTranslation(userLanguage).ifEmpty { message.text } + } ?: message.text - else -> message.text - }.trim() + else -> message.text + }.trim() - if (message.isSystem()) { - append(displayedText) - } else if (message.isPoll()) { + when { + message.isSystem() -> append(displayedText) + + message.isDeleted() -> { + if (isDirectMessaging && message.user.id == currentUser?.id) { + // DM outgoing deleted: explicit "You:" since delivery icon is absent + val youLabel = context.getString(R.string.stream_compose_channel_list_you) + append("$youLabel: ") + addStyle( + SpanStyle( + fontStyle = senderNameTextStyle.fontStyle, + fontWeight = senderNameTextStyle.fontWeight, + fontFamily = senderNameTextStyle.fontFamily, + ), + start = 0, + end = youLabel.length, + ) + } else { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + } + append(context.getString(R.string.stream_compose_message_deleted_preview)) + } + + message.isPoll() -> { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + appendInlineContent(DefaultMessagePreviewIconFactory.POLL) + append(SPACE) if (message.isPollClosed()) { append( context.getString( @@ -186,34 +219,118 @@ private class DefaultMessagePreviewFormatter( ), ) } - } else if (message.hasAudioRecording()) { + } + + message.hasAudioRecording() -> { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) appendInlineContent(DefaultMessagePreviewIconFactory.VOICE_MESSAGE) append(SPACE) append(context.getString(R.string.stream_compose_audio_recording_preview)) - } else { - appendSenderName( - message = message, - currentUser = currentUser, - senderNameTextStyle = senderNameTextStyle, - ) - if (message.hasSharedLocation()) { - appendSharedLocationMessageText( - message = message, - textStyle = messageTextStyle, - ) - } else { - appendMessageText( - messageText = displayedText, - messageTextStyle = messageTextStyle, - ) + } + + message.hasSharedLocation() -> { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + appendInlineContent(DefaultMessagePreviewIconFactory.LOCATION) + append(SPACE) + message.sharedLocation?.let { location -> + append(context.getString(location.getMessageTextResId())) } - appendAttachmentText( - attachments = message.attachments, - attachmentFactories = attachmentFactories, - attachmentTextStyle = attachmentTextFontStyle, - ) + } + + else -> { + appendSenderName(message, currentUser, senderNameTextStyle, isDirectMessaging) + appendTypedAttachmentPreview(message.attachments, displayedText) + } + } + } + } + + /** + * Appends a typed attachment preview (icon + label/caption) if the message contains + * a recognizable attachment type. Falls back to the default text + attachment formatting. + */ + private fun AnnotatedString.Builder.appendTypedAttachmentPreview( + attachments: List, + displayedText: String, + ) { + // Classify attachments — links first (images with ogUrl are links, not photos) + val links = attachments.filter { it.titleLink != null || it.ogUrl != null } + val images = attachments.filter { + it.type == AttachmentType.IMAGE && it.ogUrl == null && it.titleLink == null + } + val videos = attachments.filter { it.type == AttachmentType.VIDEO } + val files = attachments.filter { it.type == AttachmentType.FILE } + + when { + images.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.PHOTO) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = images.size, + singleLabelResId = R.string.stream_compose_photo_preview, + pluralLabelResId = R.plurals.stream_compose_photos_preview, + ) + } + + videos.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.VIDEO) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = videos.size, + singleLabelResId = R.string.stream_compose_video_preview, + pluralLabelResId = R.plurals.stream_compose_videos_preview, + ) + } + + files.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.FILE) + append(SPACE) + appendAttachmentLabel( + caption = displayedText, + count = files.size, + singleLabelResId = R.string.stream_compose_file_preview, + pluralLabelResId = R.plurals.stream_compose_files_preview, + ) + } + + links.isNotEmpty() -> { + appendInlineContent(DefaultMessagePreviewIconFactory.LINK) + append(SPACE) + if (displayedText.isNotEmpty()) { + append(displayedText) + } else { + append(context.getString(R.string.stream_compose_link_preview)) } } + + else -> { + // No recognizable typed attachment — use default text + attachment formatting + appendMessageText(displayedText, messageTextStyle) + appendAttachmentText(attachments, attachmentFactories, attachmentTextFontStyle) + } + } + } + + /** + * Appends the appropriate label for an attachment preview: + * - If caption (message text) exists: show the caption + * - If no caption and single: show type label ("Photo") + * - If no caption and multiple: show count + plural ("2 Photos") + */ + private fun AnnotatedString.Builder.appendAttachmentLabel( + caption: String, + count: Int, + singleLabelResId: Int, + pluralLabelResId: Int, + ) { + when { + caption.isNotEmpty() -> append(caption) + count > 1 -> append( + context.resources.getQuantityString(pluralLabelResId, count, count), + ) + else -> append(context.getString(singleLabelResId)) } } @@ -253,8 +370,9 @@ private class DefaultMessagePreviewFormatter( message: Message, currentUser: User?, senderNameTextStyle: TextStyle, + isDirectMessaging: Boolean = false, ) { - val sender = message.getSenderDisplayName(context, currentUser) + val sender = message.getSenderDisplayName(context, currentUser, isDirectMessaging) if (sender != null) { append("$sender: ") @@ -327,17 +445,4 @@ private class DefaultMessagePreviewFormatter( } } } - - private fun AnnotatedString.Builder.appendSharedLocationMessageText( - message: Message, - textStyle: TextStyle, - ) { - message.sharedLocation?.let { sharedLocation -> - val text = context.getString(sharedLocation.getMessageTextResId()) - appendMessageText( - messageText = text, - messageTextStyle = textStyle, - ) - } - } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt index 346aec5781d..e4ea0dad300 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory.kt @@ -16,11 +16,14 @@ package io.getstream.chat.android.compose.ui.util +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.Icon +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -52,27 +55,80 @@ public interface MessagePreviewIconFactory { internal class DefaultMessagePreviewIconFactory : MessagePreviewIconFactory { companion object { - /** - * The key for the voice message preview icon. - */ internal const val VOICE_MESSAGE = "voice_message" + internal const val PHOTO = "photo" + internal const val VIDEO = "video" + internal const val FILE = "file" + internal const val LINK = "link" + internal const val LOCATION = "location" + internal const val POLL = "poll" } + @Suppress("LongMethod") override fun createPreviewIcons(): Map { + val placeholder = Placeholder( + width = 14.sp, + height = 14.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ) + val iconModifier = Modifier.size(14.dp) return mapOf( - VOICE_MESSAGE to InlineTextContent( - placeholder = Placeholder( - width = 16.sp, - height = 16.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { + VOICE_MESSAGE to InlineTextContent(placeholder) { Icon( + modifier = iconModifier, painter = painterResource(id = R.drawable.stream_compose_ic_mic), contentDescription = null, tint = ChatTheme.colors.textSecondary, ) }, + PHOTO to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_camera), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, + VIDEO to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_video), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, + FILE to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_file), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, + LINK to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_link), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, + LOCATION to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_map_pin), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, + POLL to InlineTextContent(placeholder) { + Icon( + modifier = iconModifier, + painter = painterResource(id = R.drawable.stream_compose_ic_poll), + contentDescription = null, + tint = ChatTheme.colors.textSecondary, + ) + }, ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt index bd4ab0c8557..66682ab2beb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessageUtils.kt @@ -54,10 +54,13 @@ public fun showOriginalTextAsState(cid: String, messageId: String): State context.getString(R.string.stream_compose_channel_list_you) - else -> null + when { + user.id == currentUser?.id && isDirectMessaging -> null + user.id == currentUser?.id -> context.getString(R.string.stream_compose_channel_list_you) + isDirectMessaging -> null + else -> user.name } private val fullSizeAttachmentTypes = setOf( 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..222af8848d1 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 @@ -49,7 +49,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 +617,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. * 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 69512448b7e..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,7 +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_ic_search.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml index 95e6ea8098f..a265d01b997 100644 --- a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_search.xml @@ -15,14 +15,17 @@ limitations under the License. --> 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 afd1d6878ad..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 @@ -19,8 +19,7 @@ android:height="16dp" android:viewportWidth="16" android:viewportHeight="16"> - + 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 c7a588ac5e7..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 @@ -19,8 +19,7 @@ android:height="16dp" android:viewportWidth="16" android:viewportHeight="16"> - + 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 5e5d9c1a220..7dbcd5a9688 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -25,7 +25,25 @@ Draft: You - No channels available + Typing + %s is typing + %1$s and %2$s are typing + + %d person is typing + %d people are typing + + No conversations yet + Start a chat + + + Pin + Unpin + Mute + Unmute + Delete + More + Archive + Unarchive No results for \"%1$s\" @@ -153,8 +171,28 @@ Also send as direct message File type is not supported - + Voice message + Photo + Video + File + Link + Message deleted + No messages yet + + %d Photo + %d Photos + + + %d Video + %d Videos + + + %d File + %d Files + + + Record audio message Hold to record. Release to send Slide to cancel @@ -282,8 +320,8 @@ %d votes - 📊 Poll created: %s - 📊 Poll closed: %s + Poll created: %s + Poll closed: %s No pinned messages diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt index 51bee81da52..a1cfe2fa8d5 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelItemTest.kt @@ -25,8 +25,13 @@ import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessage import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessageSeenStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemLastMessageSentStatus import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMuted +import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMutedTrailingBottom import io.getstream.chat.android.compose.ui.channels.list.ChannelItemNoMessages import io.getstream.chat.android.compose.ui.channels.list.ChannelItemUnreadMessages +import io.getstream.chat.android.compose.ui.theme.ChannelListConfig +import io.getstream.chat.android.compose.ui.theme.ChatConfig +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition import org.junit.Rule import org.junit.Test @@ -49,6 +54,21 @@ internal class ChannelItemTest : PaparazziComposeTest { } } + @Test + fun `muted channel trailing bottom`() { + snapshotWithDarkMode { + ChatTheme( + config = ChatConfig( + channelList = ChannelListConfig( + muteIndicatorPosition = MuteIndicatorPosition.TRAILING_BOTTOM, + ), + ), + ) { + ChannelItemMutedTrailingBottom() + } + } + } + @Test fun `unread messages`() { snapshotWithDarkMode { diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelsScreenTest.kt index cb4e6427d8f..df9af51cbec 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelsScreenTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/channels/ChannelsScreenTest.kt @@ -17,9 +17,8 @@ package io.getstream.chat.android.compose.ui.channels import androidx.annotation.UiThread -import androidx.compose.ui.semantics.ProgressBarRangeInfo -import androidx.compose.ui.test.hasProgressBarRangeInfo import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import io.getstream.chat.android.client.test.MockedChatClientTest @@ -58,8 +57,7 @@ internal class ChannelsScreenTest : MockedChatClientTest { } composeTestRule.onNodeWithText("Stream Chat").assertExists() - composeTestRule.onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) - .assertExists() + composeTestRule.onNodeWithTag("Stream_ChannelListLoading").assertExists() } @Test @@ -72,8 +70,7 @@ internal class ChannelsScreenTest : MockedChatClientTest { } composeTestRule.onNodeWithText("Search").assertExists() - composeTestRule.onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) - .assertExists() + composeTestRule.onNodeWithTag("Stream_ChannelListLoading").assertExists() } @Test @@ -86,7 +83,6 @@ internal class ChannelsScreenTest : MockedChatClientTest { } composeTestRule.onNodeWithText("Search").assertExists() - composeTestRule.onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) - .assertExists() + composeTestRule.onNodeWithTag("Stream_ChannelListLoading").assertExists() } } 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..693ce4d8d97 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,15 +40,18 @@ 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, - onChannelOptionClick = {}, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), + onChannelOptionConfirm = {}, onDismiss = {}, ) } @@ -57,15 +61,18 @@ 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, - onChannelOptionClick = {}, + channelActions = listOf( + ViewInfo(channel = channel, label = "Channel Info", onAction = {}), + ), + onChannelOptionConfirm = {}, onDismiss = {}, ) } diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt index 0fe71410a47..0800c6a1d47 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt @@ -20,6 +20,7 @@ import androidx.annotation.UiThread import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.test.hasProgressBarRangeInfo import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import io.getstream.chat.android.client.test.MockedChatClientTest @@ -61,8 +62,7 @@ internal class ChatsScreenTest : MockedChatClientTest { } composeTestRule.onNodeWithText("Stream Chat").assertExists() - composeTestRule.onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) - .assertExists() + composeTestRule.onNodeWithTag("Stream_ChannelListLoading").assertExists() } @Test @@ -75,8 +75,7 @@ internal class ChatsScreenTest : MockedChatClientTest { } composeTestRule.onNodeWithText("Search").assertExists() - composeTestRule.onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)) - .assertExists() + composeTestRule.onNodeWithTag("Stream_ChannelListLoading").assertExists() } @Test 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-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png index 95f3e4c9cae..4060d04ac92 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png index 398bca409b8..c843497b70e 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_connecting.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_connecting.png index 8ec37a98033..a5d63976e54 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_connecting.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_connecting.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png index 8f3d5aeb986..62321337745 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_offline.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_offline.png index 79bf1fa90cb..f80a1c9c604 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_offline.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_offline.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_online.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_online.png index 146ef7fc646..1e9629772d6 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_online.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_online.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_connected.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_connected.png index 3ff692c164f..ba58fe123ad 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_connected.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_connected.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_offline.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_offline.png index c27538cd27b..c3cd24c7cb6 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_offline.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_offline.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_gallery_bottom_sheet.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_gallery_bottom_sheet.png index f5ad20d1e74..16c6dc2b029 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_gallery_bottom_sheet.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_gallery_bottom_sheet.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_options_menu.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_options_menu.png index 330be824ae1..897b05eda93 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_options_menu.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_options_menu.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_share_large_file_prompt.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_share_large_file_prompt.png index c98cf4f91df..7d999d9b244 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_share_large_file_prompt.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_share_large_file_prompt.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content.png index 667f69e147a..b321d106e4b 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content_in_dark_mode.png index 1ae674cde15..6b0fce1308c 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_content_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more.png index e6b6b15aa58..652105c6ed8 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more_in_dark_mode.png index e0e263da629..1a8f0abd9af 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.attachments_ChannelFilesAttachmentsContentTest_loading_more_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content.png index 82672a8e780..77981fa9521 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content_in_dark_mode.png index 4a643d6be03..d0068c29f70 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_DirectChannelInfoContentTest_content_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members.png index 4433d96dae0..2bac66eb04b 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members_in_dark_mode.png index 86fa1e85443..d11242d7f02 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_collapsed_members_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members.png index 6cc7967508a..51fd2217d90 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members_in_dark_mode.png index 98d3b0214b7..d1fe552570c 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_GroupChannelInfoContentTest_expanded_members_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_draft_message.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_draft_message.png index e0bce32a9d5..e4bf1d0bb8e 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_draft_message.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_draft_message.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_delivered_status.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_delivered_status.png index 244c001df42..aacdb24e963 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_delivered_status.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_delivered_status.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_pending_status.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_pending_status.png index d5638102e7a..cdc466fec7a 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_pending_status.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_pending_status.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_seen_status.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_seen_status.png index 3e1660762e1..174fcfb88de 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_seen_status.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_seen_status.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_sent_status.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_sent_status.png index 9fff5b1a44a..9109d65b197 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_sent_status.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_last_message_sent_status.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel.png index 8b824aa038c..23130e1b888 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel_trailing_bottom.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel_trailing_bottom.png new file mode 100644 index 00000000000..2cc9c9fd533 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel_trailing_bottom.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_no_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_no_messages.png index 074fdceedd1..950df3d08f7 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_no_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_no_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_unread_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_unread_messages.png index 05b5c8677c2..26f538ef1ea 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_unread_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_unread_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_no_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_no_user.png index 121b1b7353c..f1f3d7ea2e4 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_no_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_no_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_with_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_with_user.png index 4e11e4c0539..67ad02b07f3 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_with_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connected,_with_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_no_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_no_user.png index e7cefcf31da..20d1cbf12dc 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_no_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_no_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_with_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_with_user.png index f57a241da9f..1377be51097 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_with_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_connecting,_with_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_no_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_no_user.png index db97ba94766..d089539e490 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_no_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_no_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_with_user.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_with_user.png index 3e62f9d0c7e..5328164a2ef 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_with_user.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListHeaderTest_offline,_with_user.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_empty_channels.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_empty_channels.png index 44fb5640ea7..527feb99021 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_empty_channels.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_empty_channels.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loaded_channels.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loaded_channels.png index 47d95237f60..dde0dac7131 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loaded_channels.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loaded_channels.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_channels.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_channels.png index 5b7e53043d5..6839e2d0dea 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_channels.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_channels.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_more_channels.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_more_channels.png index c59178a4ecd..ee88c6e8954 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_more_channels.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_loading_more_channels.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_search_results.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_search_results.png index 917e5926df2..35b59225f7d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_search_results.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelListTest_search_results.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelsTest_loaded_channels.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelsTest_loaded_channels.png index bb3c3a6624f..27eac977819 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelsTest_loaded_channels.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelsTest_loaded_channels.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel.png index e2c5473ed99..3cb9740ff02 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_in_dark_mode.png index e47116a6a4c..21a2aa58fcd 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.attachments.images_ImagesPickerTest_add_more.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.attachments.images_ImagesPickerTest_add_more.png index d92a13a6b0e..08ac87a681d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.attachments.images_ImagesPickerTest_add_more.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.attachments.images_ImagesPickerTest_add_more.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_error_poll_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_error_poll_content.png index 5a249b44a51..a85e8869001 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_error_poll_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_PollMessageContentTest_error_poll_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_empty_query.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_empty_query.png index ebbae11b23f..0fe01613608 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_empty_query.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_empty_query.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_filled_query.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_filled_query.png index 3e6480ebae4..c954e5e09c0 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_filled_query.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_focused_filled_query.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_unfocused_empty_query.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_unfocused_empty_query.png index c063e41b105..b6deb24b79a 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_unfocused_empty_query.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components_SearchInputTest_unfocused_empty_query.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loaded_mention_list.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loaded_mention_list.png index 499f9b7040f..a81cb802f12 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loaded_mention_list.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loaded_mention_list.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loading_more_mention_list.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loading_more_mention_list.png index 3ab8c50e0d2..c9cdb5b5c17 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loading_more_mention_list.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.mentions_MentionListTest_loading_more_mention_list.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_EmojiMessageContentTest_error_emoji_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_EmojiMessageContentTest_error_emoji_content.png index 87350dce508..7457d813732 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_EmojiMessageContentTest_error_emoji_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_EmojiMessageContentTest_error_emoji_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png index 756185bc86c..0e41ee96e3d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style.png index 0567945dbfd..4121531e617 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style_with_visible_attachment_picker.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style_with_visible_attachment_picker.png index 318d678534e..4d29c0b6248 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style_with_visible_attachment_picker.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_default_style_with_visible_attachment_picker.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style.png index d43e9459bdc..5e7f92414ff 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_visible_attachment_picker.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_visible_attachment_picker.png index 7ad40df9715..64357ff953d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_visible_attachment_picker.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_floating_style_with_visible_attachment_picker.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_bottom_aligned_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_bottom_aligned_messages.png index ff072725ae1..890ced5384f 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_bottom_aligned_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_bottom_aligned_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png index 29e37897671..969a047f8de 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png index ac20fdd65b4..32e8a81d11d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_top_aligned_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_top_aligned_messages.png index 6c1fd07751f..1d010193928 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_top_aligned_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_top_aligned_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button.png index 474ef974b46..37bd2b9321b 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button_in_dark_mode.png index 7d07e3c83f8..c4e6cacfdfd 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_scroll_to_bottom_button_in_dark_mode.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageItemTest_pinnedMessageItem.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageItemTest_pinnedMessageItem.png index ea4b70fabc8..6fd412a37ca 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageItemTest_pinnedMessageItem.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageItemTest_pinnedMessageItem.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loaded_pinned_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loaded_pinned_messages.png index 10bf2ac9cee..76ef4105d71 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loaded_pinned_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loaded_pinned_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loading_more_pinned_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loading_more_pinned_messages.png index 10863e29951..785fd89c9ae 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loading_more_pinned_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.pinned_PinnedMessageListTest_loading_more_pinned_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png index 35e46462489..39e68854d86 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadItemTest_threadItem.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png index e6b6c4842f7..1e4c3e6606b 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png index e5fba98aa6f..2a36f4354ee 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loaded_threads_with_unread_banner.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png index 1c67dfcdba8..b5c4da02eca 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.threads_ThreadListTest_loading_more_threads.png differ 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..a56645c2b2d 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, + onChannelOptionConfirm = { 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, - onChannelOptionClick = { action -> - if (action is ViewInfo) { - // Start the channel info screen - } else { - listViewModel.performChannelAction(action) - } + channelActions = channelActions, + onChannelOptionConfirm = { 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, + onChannelOptionConfirm = { listViewModel.executeOrConfirm(it) }, onDismiss = { listViewModel.dismissChannelAction() } ) } diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt index caa311a08cd..a32df22e537 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt @@ -188,7 +188,7 @@ private object ChatThemeMessageTextFormatterDefaultSnippet : ChatThemeCustomizat } } - override fun formatMessagePreview(message: Message, currentUser: User?): AnnotatedString { + override fun formatMessagePreview(message: Message, currentUser: User?, isDirectMessaging: Boolean): AnnotatedString { return buildAnnotatedString { append(message.text) // add your custom styling here 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-common/src/main/res/values/strings.xml b/stream-chat-android-ui-common/src/main/res/values/strings.xml index 7b9a8736cbe..2d5a6a42135 100644 --- a/stream-chat-android-ui-common/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ Message is pending Message is sent Message is delivered + Message failed to send %d attachments Image attachment Video attachment 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..1fa2da39425 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,25 +156,29 @@ 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.setOnClickListener { - channelOptionClickListener?.onChannelOptionClick(option.channelAction) - dismiss() + val textStyle = if (action.isDestructive) { + style.warningItemTextStyle + } else { + style.itemTextStyle } - channelOptionTextView.setTextStyle( - if (option.isWarningItem) { - style.warningItemTextStyle - } else { - style.itemTextStyle + channelOptionTextView.text = action.label + channelOptionTextView.setStartDrawable( + requireContext().getDrawableCompat(action.icon)?.apply { + mutate() + setTint(textStyle.color) }, ) + channelOptionTextView.setOnClickListener { + channelOptionClickListener?.onChannelOptionClick(action) + dismiss() + } + channelOptionTextView.setTextStyle(textStyle) binding.optionsContainer.addView(channelOptionTextView) } 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 diff --git a/stream-chat-android-ui-components/src/test/snapshots/images/io.getstream.chat.android.ui.feature.channels.list_ChannelListViewTest_loaded_channels.png b/stream-chat-android-ui-components/src/test/snapshots/images/io.getstream.chat.android.ui.feature.channels.list_ChannelListViewTest_loaded_channels.png index 74fd1a3bbd7..a365aeb5e69 100644 Binary files a/stream-chat-android-ui-components/src/test/snapshots/images/io.getstream.chat.android.ui.feature.channels.list_ChannelListViewTest_loaded_channels.png and b/stream-chat-android-ui-components/src/test/snapshots/images/io.getstream.chat.android.ui.feature.channels.list_ChannelListViewTest_loaded_channels.png differ