diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt index 3e8b4f4db06..988d380c82a 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/data/CustomSettings.kt @@ -33,6 +33,7 @@ class CustomSettings(private val context: Context) { var isAdaptiveLayoutEnabled: Boolean by booleanPref(AdaptiveLayout) + var isChannelPinningEnabled: Boolean by booleanPref(ChannelPinning) var isComposerLinkPreviewEnabled: Boolean by booleanPref(ComposerLinkPreview) var isComposerFloatingStyleEnabled: Boolean by booleanPref(ComposerFloatingStyle) var isSystemAttachmentPickerEnabled: Boolean by booleanPref(SystemAttachmentPicker) @@ -48,6 +49,7 @@ class CustomSettings(private val context: Context) { } private const val AdaptiveLayout = "adaptive_layout" +private const val ChannelPinning = "channel_pinning" private const val ComposerLinkPreview = "composer_link_preview" private const val ComposerFloatingStyle = "composer_floating_style" private const val SystemAttachmentPicker = "system_attachment_picker" 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 bd4a99ce1ed..2c465c2bf82 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 @@ -61,6 +61,7 @@ import io.getstream.chat.android.client.api.models.QueryThreadsRequest import io.getstream.chat.android.client.api.state.globalStateFlow import io.getstream.chat.android.compose.sample.ChatHelper import io.getstream.chat.android.compose.sample.R +import io.getstream.chat.android.compose.sample.data.customSettings import io.getstream.chat.android.compose.sample.feature.channel.add.AddChannelActivity import io.getstream.chat.android.compose.sample.feature.channel.add.group.AddGroupChannelActivity import io.getstream.chat.android.compose.sample.feature.channel.isGroupChannel @@ -81,12 +82,15 @@ import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenu 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.ChannelOptionsVisibility 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.ChannelListConfig import io.getstream.chat.android.compose.ui.theme.ChannelListDividerItemParams import io.getstream.chat.android.compose.ui.theme.ChannelListItemContentParams import io.getstream.chat.android.compose.ui.theme.ChatComponentFactory import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.ChatUiConfig import io.getstream.chat.android.compose.ui.threads.ThreadsScreen import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModelFactory @@ -106,6 +110,8 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) class ChannelsActivity : ComponentActivity() { + private val settings by lazy { customSettings() } + /** * The provided predefined filter has the following specs: * @@ -120,7 +126,7 @@ class ChannelsActivity : ComponentActivity() { * * **Sort:** * ``` - * QuerySortByField.descByName("last_updated") + * QuerySortByField().desc("pinned_at").desc("last_updated") * ``` */ private val channelsViewModelFactory by lazy { @@ -161,7 +167,15 @@ class ChannelsActivity : ComponentActivity() { val unreadChannelsCount by unreadChannelsCountFlow.collectAsStateWithLifecycle(0) val unreadThreadsCount by unreadThreadsCountFlow.collectAsStateWithLifecycle(0) - SampleChatTheme { + SampleChatTheme( + config = ChatUiConfig( + channelList = ChannelListConfig( + optionsVisibility = ChannelOptionsVisibility( + isPinChannelVisible = settings.isChannelPinningEnabled, + ), + ), + ), + ) { val user by channelsViewModel.user.collectAsStateWithLifecycle() val drawerState = rememberDrawerState(DrawerValue.Closed) val coroutineScope = rememberCoroutineScope() @@ -348,7 +362,6 @@ class ChannelsActivity : ComponentActivity() { if (selectedChannel != null) { val channelActions = buildDefaultChannelActions( selectedChannel = selectedChannel, - isMuted = channelsViewModel.isChannelMuted(selectedChannel.cid), ownCapabilities = selectedChannel.ownCapabilities, viewModel = channelsViewModel, onViewInfoAction = ::viewChannelInfo, diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt index 12227003d9f..dcc5857f091 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/chats/ChatsActivity.kt @@ -51,7 +51,6 @@ import io.getstream.chat.android.client.api.state.globalStateFlow import io.getstream.chat.android.compose.sample.ChatHelper import io.getstream.chat.android.compose.sample.R import io.getstream.chat.android.compose.sample.data.customSettings -import io.getstream.chat.android.compose.sample.feature.channel.ChannelConstants.CHANNEL_ARG_DRAFT import io.getstream.chat.android.compose.sample.feature.channel.add.AddChannelActivity import io.getstream.chat.android.compose.sample.feature.channel.isGroupChannel import io.getstream.chat.android.compose.sample.feature.channel.list.CustomChatEventHandlerFactory @@ -92,9 +91,7 @@ import io.getstream.chat.android.compose.viewmodel.pinned.PinnedMessageListViewM import io.getstream.chat.android.compose.viewmodel.pinned.PinnedMessageListViewModelFactory import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.Channel -import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.Message -import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.ui.common.feature.channel.attachments.ChannelAttachmentsViewEvent import io.getstream.chat.android.ui.common.feature.channel.info.ChannelInfoViewEvent import io.getstream.chat.android.ui.common.state.channel.info.ChannelInfoViewState @@ -131,16 +128,34 @@ class ChatsActivity : ComponentActivity() { private val messageId by lazy { intent.getStringExtra(KEY_MESSAGE_ID) } private val parentMessageId by lazy { intent.getStringExtra(KEY_PARENT_MESSAGE_ID) } + private val settings by lazy { customSettings() } + + /** + * The provided predefined filter has the following specs: + * + * **Filter:** + * ``` + * Filters.and( + * Filters.eq("type", "messaging"), + * Filters.`in`("members", listOf(currentUserId)), + * Filters.or(Filters.notExists("draft"), Filters.eq("draft", false)), + * ) + * ``` + * + * **Sort:** + * ``` + * QuerySortByField().desc("pinned_at").desc("last_updated") + * ``` + */ private val channelListViewModelFactory by lazy { val chatClient = ChatClient.instance() val currentUserId = chatClient.getCurrentUser()?.id ?: "" ChannelListViewModelFactory( chatClient = chatClient, - querySort = QuerySortByField.descByName("last_updated"), - filters = Filters.and( - Filters.eq("type", "messaging"), - Filters.`in`("members", listOf(currentUserId)), - Filters.or(Filters.notExists(CHANNEL_ARG_DRAFT), Filters.eq(CHANNEL_ARG_DRAFT, false)), + predefinedFilterName = "android_sample_filter", + filterValues = mapOf( + "channel_type" to "messaging", + "user_id" to currentUserId, ), chatEventHandlerFactory = CustomChatEventHandlerFactory(), ) @@ -165,6 +180,7 @@ class ChatsActivity : ComponentActivity() { channelList = ChannelListConfig( optionsVisibility = ChannelOptionsVisibility( isViewInfoVisible = AdaptiveLayoutInfo.singlePaneWindow(), + isPinChannelVisible = settings.isChannelPinningEnabled, ), ), ), @@ -608,7 +624,7 @@ class ChatsActivity : ComponentActivity() { ) = ChannelViewModelFactory( context = applicationContext, channelId = channelId, - composerOptions = ComposerOptions(linkPreviewEnabled = customSettings().isComposerLinkPreviewEnabled), + composerOptions = ComposerOptions(linkPreviewEnabled = settings.isComposerLinkPreviewEnabled), messageId = messageId, parentMessageId = parentMessageId, ) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt index f655457b46e..49f47056942 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/login/CustomLoginActivity.kt @@ -126,6 +126,9 @@ class CustomLoginActivity : AppCompatActivity() { var userTokenText by remember { mutableStateOf("") } var userNameText by remember { mutableStateOf("") } var isAdaptiveLayoutEnabled by remember { mutableStateOf(settings.isAdaptiveLayoutEnabled) } + var isChannelPinningEnabled by remember { + mutableStateOf(settings.isChannelPinningEnabled) + } var isComposerLinkPreviewEnabled by remember { mutableStateOf(settings.isComposerLinkPreviewEnabled) } @@ -150,6 +153,15 @@ class CustomLoginActivity : AppCompatActivity() { settings.isAdaptiveLayoutEnabled = it }, ), + FeatureFlag( + label = stringResource(R.string.custom_login_flag_channel_pinning_label), + description = stringResource(R.string.custom_login_flag_channel_pinning_description), + value = isChannelPinningEnabled, + onValueChange = { + isChannelPinningEnabled = it + settings.isChannelPinningEnabled = it + }, + ), FeatureFlag( label = stringResource(R.string.custom_login_flag_composer_link_preview_label), description = stringResource( diff --git a/stream-chat-android-compose-sample/src/main/res/values/strings.xml b/stream-chat-android-compose-sample/src/main/res/values/strings.xml index fdb8a887f28..7683bc8327a 100644 --- a/stream-chat-android-compose-sample/src/main/res/values/strings.xml +++ b/stream-chat-android-compose-sample/src/main/res/values/strings.xml @@ -41,6 +41,8 @@ Show link previews in the message composer System attachment picker Use the system\'s native file/media picker + Channel pinning + Show the Pin/Unpin Chat action in the channel options menu Pinned Messages 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 c1ae84f0736..edbc38ca227 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -881,6 +881,7 @@ public final class io/getstream/chat/android/compose/ui/channels/header/Composab public final class io/getstream/chat/android/compose/ui/channels/info/ComposableSingletons$SelectedChannelMenuKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/channels/info/ComposableSingletons$SelectedChannelMenuKt; public fun ()V + public final fun getLambda$1458176636$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1837108307$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$835101941$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } @@ -902,14 +903,23 @@ public final class io/getstream/chat/android/compose/ui/channels/list/Composable public static final field INSTANCE Lio/getstream/chat/android/compose/ui/channels/list/ComposableSingletons$ChannelItemKt; public fun ()V public final fun getLambda$-1095060318$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-1232665624$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-262212850$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$-781924446$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-851243681$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$1003502438$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$112526037$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$1228594335$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1334723625$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1340453964$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1561934893$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$1593816240$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$1946891593$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$2130050484$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$2144537814$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$230028639$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$468999837$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$481395312$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$633588870$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } @@ -1210,6 +1220,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 buildDefaultChannelActions (Lio/getstream/chat/android/models/Channel;Ljava/util/Set;Lio/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel;Lkotlin/jvm/functions/Function1;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; } @@ -2895,16 +2906,18 @@ public final class io/getstream/chat/android/compose/ui/theme/ChannelItemUnreadC 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;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;)V - public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;)V + public synthetic fun (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; - public final fun component2 ()Z - public final fun component3 ()Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility; - public final fun copy (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;)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;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/ChannelListConfig; + public final fun component2 ()Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; + public final fun component3 ()Z + public final fun component4 ()Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility; + public final fun copy (Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition;Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;)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;Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition;ZLio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility;ILjava/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 getOptionsVisibility ()Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionsVisibility; + public final fun getPinIndicatorPosition ()Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; public final fun getSwipeActionsEnabled ()Z public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -5523,6 +5536,14 @@ public final class io/getstream/chat/android/compose/ui/theme/MuteIndicatorPosit public static fun values ()[Lio/getstream/chat/android/compose/ui/theme/MuteIndicatorPosition; } +public final class io/getstream/chat/android/compose/ui/theme/PinIndicatorPosition : java/lang/Enum { + public static final field InlineTitle Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; + public static final field TrailingBottom Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; + public static fun values ()[Lio/getstream/chat/android/compose/ui/theme/PinIndicatorPosition; +} + public final class io/getstream/chat/android/compose/ui/theme/PinnedMessageListEmptyContentParams { public static final field $stable I public fun ()V 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 f0796be33b9..e2a07692642 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 @@ -207,7 +207,6 @@ public fun ChannelsScreen( val channel = lastChannel.value val channelActions = buildDefaultChannelActions( selectedChannel = channel, - isMuted = listViewModel.isChannelMuted(channel.cid), ownCapabilities = channel.ownCapabilities, viewModel = listViewModel, onViewInfoAction = { ch -> 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 a0518c1c1fa..2e8815ae627 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 @@ -16,6 +16,9 @@ package io.getstream.chat.android.compose.ui.channels.info +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -26,6 +29,7 @@ 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.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -33,9 +37,15 @@ 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.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource 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.client.extensions.isMutedFor +import io.getstream.chat.android.client.extensions.isPinned +import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.components.SimpleMenu import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize import io.getstream.chat.android.compose.ui.theme.ChannelAvatarParams @@ -43,13 +53,17 @@ import io.getstream.chat.android.compose.ui.theme.ChannelMenuCenterContentParams import io.getstream.chat.android.compose.ui.theme.ChannelMenuHeaderContentParams 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.dmCounterpartId import io.getstream.chat.android.compose.ui.util.getMembersStatusText import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChannelMute +import io.getstream.chat.android.models.Member 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 +import java.util.Date /** * Shows special UI when an item is selected. @@ -111,6 +125,9 @@ public fun SelectedChannelMenu( /** * Represents the default content shown at the top of [SelectedChannelMenu] dialog. * + * Renders inline muted and pinned icons next to the channel name based on the channel's pin state + * and the current user's mute settings. When [currentUser] is `null`, no state icons are rendered. + * * @param selectedChannel The channel the user selected. * @param currentUser The currently logged-in user data. */ @@ -119,6 +136,8 @@ internal fun DefaultSelectedChannelMenuHeaderContent( selectedChannel: Channel, currentUser: User?, ) { + val showPinnedIcon = selectedChannel.isPinned() + val showMutedIcon = currentUser != null && isChannelOrCounterpartMuted(selectedChannel, currentUser) Row( modifier = Modifier .fillMaxWidth() @@ -144,15 +163,11 @@ internal fun DefaultSelectedChannelMenuHeaderContent( .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, + HeaderTitleRow( + selectedChannel = selectedChannel, + currentUser = currentUser, + showMutedIcon = showMutedIcon, + showPinnedIcon = showPinnedIcon, ) Text( text = selectedChannel.getMembersStatusText( @@ -168,61 +183,163 @@ internal fun DefaultSelectedChannelMenuHeaderContent( } } -/** - * Preview of [SelectedChannelMenu] styled as a centered modal dialog. - * - * Should show a centered dialog with channel members and channel options. - */ -@Preview(showBackground = true, name = "SelectedChannelMenu Preview (Centered dialog)") +private fun isChannelOrCounterpartMuted(channel: Channel, currentUser: User): Boolean { + if (channel.isMutedFor(currentUser)) return true + val otherUserId = channel.dmCounterpartId(currentUser) ?: return false + return currentUser.mutes.any { it.target?.id == otherUserId } +} + @Composable -private fun SelectedChannelMenuCenteredDialogPreview() { - ChatTheme { - Box(modifier = Modifier.fillMaxSize()) { - val channel = PreviewChannelData.channelWithManyMembers - SelectedChannelMenu( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .wrapContentHeight() - .align(Alignment.Center), - shape = RoundedCornerShape(16.dp), - selectedChannel = channel, - currentUser = PreviewUserData.user1, - channelActions = listOf( - ViewInfo(channel = channel, label = "Channel Info", onAction = {}), - ), - onChannelOptionConfirm = {}, - onDismiss = {}, +private fun HeaderTitleRow( + selectedChannel: Channel, + currentUser: User?, + showMutedIcon: Boolean, + showPinnedIcon: Boolean, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs), + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = ChatTheme.channelNameFormatter.formatChannelName(selectedChannel, currentUser), + style = ChatTheme.typography.headingSmall, + color = ChatTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (showMutedIcon) { + HeaderStateIcon( + iconRes = R.drawable.stream_design_ic_mute, + contentDescriptionRes = R.string.stream_compose_channel_item_muted, + testTag = "Stream_ChannelMenuHeaderMutedIcon", + ) + } + if (showPinnedIcon) { + HeaderStateIcon( + iconRes = R.drawable.stream_design_ic_pin, + contentDescriptionRes = R.string.stream_compose_channel_item_pinned, + testTag = "Stream_ChannelMenuHeaderPinnedIcon", ) } } } +@Composable +private fun HeaderStateIcon( + @DrawableRes iconRes: Int, + @StringRes contentDescriptionRes: Int, + testTag: String, +) { + Icon( + modifier = Modifier + .testTag(testTag) + .size(16.dp), + painter = painterResource(id = iconRes), + contentDescription = stringResource(contentDescriptionRes), + tint = ChatTheme.colors.textTertiary, + ) +} + +@Preview(showBackground = true) +@Composable +private fun SelectedChannelMenuCenteredDialogPreview() { + ChatTheme { + SelectedChannelMenuCenteredDialog() + } +} + +@Composable +internal fun SelectedChannelMenuCenteredDialog() { + SelectedChannelMenuSample( + alignment = Alignment.Center, + modifier = Modifier.padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) +} + +@Preview(showBackground = true) +@Composable +private fun SelectedChannelMenuBottomSheetDialogPreview() { + ChatTheme { + SelectedChannelMenuBottomSheetDialog() + } +} + +@Composable +internal fun SelectedChannelMenuBottomSheetDialog() { + SelectedChannelMenuSample( + alignment = Alignment.BottomCenter, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) +} + +@Preview(showBackground = true) +@Composable +private fun SelectedChannelMenuMutedPinnedPreview() { + ChatTheme { + SelectedChannelMenuMutedPinned() + } +} + +@Composable +internal fun SelectedChannelMenuMutedPinned() { + val baseChannel = PreviewChannelData.channelWithManyMembers + val pinnedChannel = baseChannel.copy( + membership = Member(user = PreviewUserData.user1, pinnedAt = Date()), + ) + val mutedUser = PreviewUserData.user1.copy( + channelMutes = listOf( + ChannelMute( + user = PreviewUserData.user1, + channel = pinnedChannel, + createdAt = Date(), + updatedAt = Date(), + expires = null, + ), + ), + ) + SelectedChannelMenuSample( + alignment = Alignment.BottomCenter, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + channel = pinnedChannel, + currentUser = mutedUser, + ) +} + /** - * Preview of [SelectedChannelMenu] styled as a bottom sheet dialog. + * Renders a [SelectedChannelMenu] over a full-size [Box] using preview sample data. * - * Should show a bottom sheet dialog with channel members and channel options. + * @param alignment Vertical alignment of the menu inside the parent [Box]. + * @param shape The shape of the menu surface. + * @param modifier Modifier applied to the menu, before `fillMaxWidth`, `wrapContentHeight` and + * `align` are chained on. + * @param channel The channel rendered in the menu. + * @param currentUser The user used to resolve member status text and to derive the inline state + * icons (muted, pinned) in the default header. */ -@Preview(showBackground = true, name = "SelectedChannelMenu Preview (Bottom sheet dialog)") @Composable -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 = channel, - currentUser = PreviewUserData.user1, - channelActions = listOf( - ViewInfo(channel = channel, label = "Channel Info", onAction = {}), - ), - onChannelOptionConfirm = {}, - onDismiss = {}, - ) - } +private fun SelectedChannelMenuSample( + alignment: Alignment, + shape: Shape, + modifier: Modifier = Modifier, + channel: Channel = PreviewChannelData.channelWithManyMembers, + currentUser: User? = PreviewUserData.user1, +) { + Box(modifier = Modifier.fillMaxSize()) { + SelectedChannelMenu( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .align(alignment), + shape = shape, + selectedChannel = channel, + currentUser = currentUser, + 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 e179650a888..ba0908d52b7 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 @@ -38,6 +38,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -53,6 +54,7 @@ import androidx.compose.ui.unit.dp 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.extensions.isPinned 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 @@ -69,8 +71,9 @@ import io.getstream.chat.android.compose.ui.theme.ChannelItemTrailingContentPara import io.getstream.chat.android.compose.ui.theme.ChannelItemUnreadCountIndicatorParams import io.getstream.chat.android.compose.ui.theme.ChannelListConfig import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.theme.ChatUiConfig +import io.getstream.chat.android.compose.ui.theme.LocalChatUiConfig import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition +import io.getstream.chat.android.compose.ui.theme.PinIndicatorPosition 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 @@ -78,6 +81,7 @@ 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.Member import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus import io.getstream.chat.android.models.User @@ -255,7 +259,9 @@ private fun TitleRow( ) { val channel = channelItemState.channel val isMuted = channelItemState.isMuted || channelItemState.isUserMuted + val isPinned = channel.isPinned() val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + val pinPosition = ChatTheme.config.channelList.pinIndicatorPosition Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -278,14 +284,10 @@ private fun TitleRow( ) if (isMuted && mutePosition == MuteIndicatorPosition.InlineTitle) { - Icon( - modifier = Modifier - .testTag("Stream_ChannelMutedIcon") - .size(16.dp), - painter = painterResource(id = R.drawable.stream_design_ic_mute), - contentDescription = stringResource(R.string.stream_compose_channel_item_muted), - tint = ChatTheme.colors.textTertiary, - ) + MutedIcon() + } + if (isPinned && pinPosition == PinIndicatorPosition.InlineTitle) { + PinnedIcon() } } @@ -324,6 +326,7 @@ private fun MessageRow( val isDirectMessaging = channel.isOneToOne(currentUser) val isLastMessageFromCurrentUser = lastMessage?.user?.id == currentUser?.id val mutePosition = ChatTheme.config.channelList.muteIndicatorPosition + val pinPosition = ChatTheme.config.channelList.pinIndicatorPosition Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -347,18 +350,38 @@ private fun MessageRow( if ((channelItemState.isMuted || channelItemState.isUserMuted) && mutePosition == MuteIndicatorPosition.TrailingBottom ) { - Icon( - modifier = Modifier - .testTag("Stream_ChannelMutedIcon") - .size(16.dp), - painter = painterResource(id = R.drawable.stream_design_ic_mute), - contentDescription = stringResource(R.string.stream_compose_channel_item_muted), - tint = ChatTheme.colors.textTertiary, - ) + MutedIcon() + } + if (channel.isPinned() && pinPosition == PinIndicatorPosition.TrailingBottom) { + PinnedIcon() } } } +@Composable +private fun MutedIcon() { + Icon( + modifier = Modifier + .testTag("Stream_ChannelMutedIcon") + .size(16.dp), + painter = painterResource(id = R.drawable.stream_design_ic_mute), + contentDescription = stringResource(R.string.stream_compose_channel_item_muted), + tint = ChatTheme.colors.textTertiary, + ) +} + +@Composable +private fun PinnedIcon() { + Icon( + modifier = Modifier + .testTag("Stream_ChannelPinnedIcon") + .size(16.dp), + painter = painterResource(id = R.drawable.stream_design_ic_pin), + contentDescription = stringResource(R.string.stream_compose_channel_item_pinned), + tint = ChatTheme.colors.textTertiary, + ) +} + @Composable private fun RowScope.MessageContent( channelItemState: ItemState.ChannelItemState, @@ -490,21 +513,138 @@ internal fun ChannelItemMuted() { @Preview(showBackground = true) @Composable private fun ChannelItemMutedTrailingBottomPreview() { - ChatTheme( - config = ChatUiConfig( - channelList = ChannelListConfig(muteIndicatorPosition = MuteIndicatorPosition.TrailingBottom), - ), - ) { + ChatTheme { ChannelItemMutedTrailingBottom() } } @Composable internal fun ChannelItemMutedTrailingBottom() { + WithChannelListConfig( + config = ChatTheme.config.channelList.copy( + muteIndicatorPosition = MuteIndicatorPosition.TrailingBottom, + ), + ) { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isMuted = true, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ChannelItemPinnedPreview() { + ChatTheme { + ChannelItemPinned() + } +} + +@Composable +internal fun ChannelItemPinned() { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isPinned = true, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ChannelItemPinnedTrailingBottomPreview() { + ChatTheme { + ChannelItemPinnedTrailingBottom() + } +} + +@Composable +internal fun ChannelItemPinnedTrailingBottom() { + WithChannelListConfig( + config = ChatTheme.config.channelList.copy( + pinIndicatorPosition = PinIndicatorPosition.TrailingBottom, + ), + ) { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isPinned = true, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ChannelItemMutedPinnedPreview() { + ChatTheme { + ChannelItemMutedPinned() + } +} + +@Composable +internal fun ChannelItemMutedPinned() { ChannelItem( currentUser = PreviewUserData.user1, channel = PreviewChannelData.channelWithMessages, isMuted = true, + isPinned = true, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ChannelItemMutedPinnedTrailingBottomPreview() { + ChatTheme { + ChannelItemMutedPinnedTrailingBottom() + } +} + +@Composable +internal fun ChannelItemMutedPinnedTrailingBottom() { + WithChannelListConfig( + config = ChatTheme.config.channelList.copy( + muteIndicatorPosition = MuteIndicatorPosition.TrailingBottom, + pinIndicatorPosition = PinIndicatorPosition.TrailingBottom, + ), + ) { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isMuted = true, + isPinned = true, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ChannelItemMutedPinnedMixedPreview() { + ChatTheme { + ChannelItemMutedPinnedMixed() + } +} + +@Composable +internal fun ChannelItemMutedPinnedMixed() { + WithChannelListConfig( + config = ChatTheme.config.channelList.copy( + pinIndicatorPosition = PinIndicatorPosition.TrailingBottom, + ), + ) { + ChannelItem( + currentUser = PreviewUserData.user1, + channel = PreviewChannelData.channelWithMessages, + isMuted = true, + isPinned = true, + ) + } +} + +@Composable +private fun WithChannelListConfig(config: ChannelListConfig, content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalChatUiConfig provides ChatTheme.config.copy(channelList = config), + content = content, ) } @@ -635,12 +775,23 @@ private fun ChannelItem( currentUser: User?, channel: Channel, isMuted: Boolean = false, + isPinned: Boolean = false, draftMessage: DraftMessage? = null, isSelected: Boolean = false, ) { + val effectiveChannel = if (isPinned) { + channel.copy( + membership = Member( + user = currentUser ?: PreviewUserData.user1, + pinnedAt = Date(), + ), + ) + } else { + channel + } ChannelItem( channelItem = ItemState.ChannelItemState( - channel = channel, + channel = effectiveChannel, isMuted = isMuted, draftMessage = draftMessage, isSelected = isSelected, 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 a6e167bc295..fb905751916 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 @@ -33,6 +33,7 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.theme.ChannelOptionsItemParams 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.dmCounterpartId import io.getstream.chat.android.compose.ui.util.isDistinct import io.getstream.chat.android.compose.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.models.Channel @@ -94,22 +95,22 @@ public fun ChannelOptions( * Each action is self-describing and carries its icon, label, and execution handler. * * Actions vary by channel type: - * - **DM:** View Info, Mute/Unmute User, Block/Unblock User, Archive Chat, Delete Chat - * - **Group (owner):** View Info, Archive Group, Delete Group - * - **Group (member):** View Info, Archive Group, Leave Group + * - **DM:** View Info, Pin/Unpin Chat, Mute/Unmute User, Block/Unblock User, Archive/Unarchive Chat, Delete Chat + * - **Group (owner):** View Info, Pin/Unpin Chat, Archive/Unarchive Chat, Leave Group, Delete Group + * - **Group (member):** View Info, Pin/Unpin Chat, Archive/Unarchive Chat, Leave Group + * + * Pin and Archive actions are opt-in via [ChannelOptionsVisibility.isPinChannelVisible] and + * [ChannelOptionsVisibility.isArchiveChannelVisible] respectively. * * @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. * @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", "LongParameterList") @Composable public fun buildDefaultChannelActions( selectedChannel: Channel, - isMuted: Boolean, ownCapabilities: Set, viewModel: ChannelListViewModel, onViewInfoAction: (Channel) -> Unit, @@ -144,9 +145,42 @@ public fun buildDefaultChannelActions( } } +/** + * Deprecated overload that forwarded an `isMuted` argument never read by the action builders. Use + * the overload without `isMuted` instead. + * + * @param selectedChannel The currently selected channel. + * @param isMuted Unused. Kept for source compatibility; the value is ignored. + * @param ownCapabilities Set of capabilities the user is given for the current channel. + * @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. + */ +@Deprecated( + message = "The isMuted parameter is unused and will be removed in a future release.", + replaceWith = ReplaceWith( + "buildDefaultChannelActions(selectedChannel, ownCapabilities, viewModel, onViewInfoAction)", + ), + level = DeprecationLevel.WARNING, +) +@Suppress("UNUSED_PARAMETER", "LongParameterList") +@Composable +public fun buildDefaultChannelActions( + selectedChannel: Channel, + isMuted: Boolean, + ownCapabilities: Set, + viewModel: ChannelListViewModel, + onViewInfoAction: (Channel) -> Unit, +): List = buildDefaultChannelActions( + selectedChannel = selectedChannel, + ownCapabilities = ownCapabilities, + viewModel = viewModel, + onViewInfoAction = onViewInfoAction, +) + /** * Builds channel actions for DM (1-to-1) channels. - * Shows: View Info, Mute/Unmute User, Block/Unblock User, Archive Chat, Delete Chat. + * Shows: View Info, Pin/Unpin Chat, Mute/Unmute User, Block/Unblock User, Archive/Unarchive Chat, Delete Chat. */ @Suppress("LongParameterList") @Composable @@ -159,7 +193,7 @@ private fun buildDmChannelActions( viewModel: ChannelListViewModel, onViewInfoAction: (Channel) -> Unit, ): List { - val otherUserId = selectedChannel.members.firstOrNull { it.user.id != currentUser?.id }?.user?.id + val otherUserId = selectedChannel.dmCounterpartId(currentUser) val canDeleteChannel = ownCapabilities.contains(ChannelCapabilities.DELETE_CHANNEL) return listOfNotNull( @@ -168,6 +202,11 @@ private fun buildDmChannelActions( selectedChannel = selectedChannel, onViewInfoAction = onViewInfoAction, ), + buildDmPinAction( + canPinChannel = optionVisibility.isPinChannelVisible, + selectedChannel = selectedChannel, + viewModel = viewModel, + ), buildDmMuteUserAction( isVisible = optionVisibility.isMuteUserVisible, otherUserId = otherUserId, @@ -179,11 +218,6 @@ private fun buildDmChannelActions( selectedChannel = selectedChannel, viewModel = viewModel, ), - buildDmPinAction( - canPinChannel = optionVisibility.isPinChannelVisible, - selectedChannel = selectedChannel, - viewModel = viewModel, - ), buildDmArchiveAction( canArchiveChannel = optionVisibility.isArchiveChannelVisible, selectedChannel = selectedChannel, @@ -290,8 +324,8 @@ private fun buildDmDeleteAction( /** * Builds channel actions for group channels. - * - **Owner (has DELETE_CHANNEL):** View Info, Archive Group, Delete Group - * - **Member (no DELETE_CHANNEL):** View Info, Archive Group, Leave Group + * - **Owner (has DELETE_CHANNEL):** View Info, Pin/Unpin Chat, Archive/Unarchive Chat, Leave Group, Delete Group + * - **Member (no DELETE_CHANNEL):** View Info, Pin/Unpin Chat, Archive/Unarchive Chat, Leave Group */ @Suppress("LongMethod", "LongParameterList") @Composable diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatUiConfig.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatUiConfig.kt index 91a078dfe5d..7f9b7011b17 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatUiConfig.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatUiConfig.kt @@ -40,15 +40,28 @@ public enum class MuteIndicatorPosition { TrailingBottom, } +/** + * Defines where the pin indicator icon is placed in the channel list item. + */ +public enum class PinIndicatorPosition { + /** Icon appears inline after the channel name in the title row. */ + InlineTitle, + + /** Icon appears at the trailing end of the message/preview row. */ + TrailingBottom, +} + /** * Behavioral configuration for the channel list. * * @param muteIndicatorPosition Where the mute icon is placed in the channel list item. + * @param pinIndicatorPosition Where the pin icon is placed in the channel list item. * @param swipeActionsEnabled Whether swipe-to-reveal actions are enabled on channel list items. * @param optionsVisibility Controls which options are visible in the channel options menu. */ public data class ChannelListConfig( val muteIndicatorPosition: MuteIndicatorPosition = MuteIndicatorPosition.InlineTitle, + val pinIndicatorPosition: PinIndicatorPosition = PinIndicatorPosition.InlineTitle, val swipeActionsEnabled: Boolean = true, val optionsVisibility: ChannelOptionsVisibility = ChannelOptionsVisibility(), ) 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 70259436773..edea678e950 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 @@ -120,6 +120,18 @@ public fun Channel.getMembersStatusText( ) } +/** + * Returns the user id of the DM counterpart for this channel — the other member of a one-to-one + * conversation with [currentUser]. Returns `null` when the channel is not a one-to-one (see + * [isOneToOne]) or when the counterpart can't be resolved. + * + * @param currentUser The currently logged in user. + */ +internal fun Channel.dmCounterpartId(currentUser: User?): String? { + if (!isOneToOne(currentUser)) return null + return members.firstOrNull { it.user.id != currentUser?.id }?.user?.id +} + /** * Returns a list of users that are members of the channel excluding the currently * logged in user. diff --git a/stream-chat-android-compose/src/main/res/values-es/strings.xml b/stream-chat-android-compose/src/main/res/values-es/strings.xml index 291e0079939..0e328588b14 100644 --- a/stream-chat-android-compose/src/main/res/values-es/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-es/strings.xml @@ -63,6 +63,7 @@ "Bloquear usuario" "Cancelar" "silenciado" + "fijado" "Abrir conversación" "Abrir opciones de conversación" "Botón de reproducción" diff --git a/stream-chat-android-compose/src/main/res/values-fr/strings.xml b/stream-chat-android-compose/src/main/res/values-fr/strings.xml index ec2c346199f..7f2816a1484 100644 --- a/stream-chat-android-compose/src/main/res/values-fr/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-fr/strings.xml @@ -63,6 +63,7 @@ "Bloquer l\'utilisateur" "Annuler" "en sourdine" + "épinglé" "Ouvrir la conversation" "Ouvrir les options de conversation" "Bouton de lecture" diff --git a/stream-chat-android-compose/src/main/res/values-hi/strings.xml b/stream-chat-android-compose/src/main/res/values-hi/strings.xml index b031e5c08b9..7895f79f6ba 100644 --- a/stream-chat-android-compose/src/main/res/values-hi/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-hi/strings.xml @@ -123,6 +123,7 @@ "उपयोगकर्ता को ब्लॉक करें" "रद्द करें" "म्यूट किया गया" + "पिन किया गया" "बातचीत खोलें" "बातचीत के विकल्प खोलें" "चलाएँ बटन" diff --git a/stream-chat-android-compose/src/main/res/values-in/strings.xml b/stream-chat-android-compose/src/main/res/values-in/strings.xml index c517c558c10..4a95f6f35a5 100644 --- a/stream-chat-android-compose/src/main/res/values-in/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-in/strings.xml @@ -63,6 +63,7 @@ "Blokir pengguna" "Batal" "dibisukan" + "disematkan" "Buka percakapan" "Buka opsi percakapan" "Tombol putar" diff --git a/stream-chat-android-compose/src/main/res/values-it/strings.xml b/stream-chat-android-compose/src/main/res/values-it/strings.xml index fcf558cf0fb..f1c60dd3d8d 100644 --- a/stream-chat-android-compose/src/main/res/values-it/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-it/strings.xml @@ -123,6 +123,7 @@ "Blocca utente" "Annulla" "silenziato" + "fissato" "Apri conversazione" "Apri opzioni conversazione" "Pulsante riproduci" diff --git a/stream-chat-android-compose/src/main/res/values-ja/strings.xml b/stream-chat-android-compose/src/main/res/values-ja/strings.xml index fb988edc981..553a50bcb49 100644 --- a/stream-chat-android-compose/src/main/res/values-ja/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-ja/strings.xml @@ -63,6 +63,7 @@ "ユーザーをブロック" "キャンセル" "ミュート中" + "ピン留め中" "会話を開く" "会話のオプションを開く" "再生ボタン" diff --git a/stream-chat-android-compose/src/main/res/values-ko/strings.xml b/stream-chat-android-compose/src/main/res/values-ko/strings.xml index 50e1710715b..8506f63ca69 100644 --- a/stream-chat-android-compose/src/main/res/values-ko/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-ko/strings.xml @@ -63,6 +63,7 @@ "사용자 차단" "취소" "음소거됨" + "고정됨" "대화 열기" "대화 옵션 열기" "재생 버튼" 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 b2761f4e458..20d4ebaacf8 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -311,6 +311,7 @@ Open conversation Open conversation options muted + pinned %d unread message %d unread 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 cd19b563952..e8888260e62 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,13 +25,14 @@ 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.ChannelItemMutedPinned +import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMutedPinnedMixed +import io.getstream.chat.android.compose.ui.channels.list.ChannelItemMutedPinnedTrailingBottom 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.ChannelItemPinned +import io.getstream.chat.android.compose.ui.channels.list.ChannelItemPinnedTrailingBottom 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.ChatTheme -import io.getstream.chat.android.compose.ui.theme.ChatUiConfig -import io.getstream.chat.android.compose.ui.theme.MuteIndicatorPosition import org.junit.Rule import org.junit.Test @@ -57,15 +58,42 @@ internal class ChannelItemTest : PaparazziComposeTest { @Test fun `muted channel trailing bottom`() { snapshotWithDarkMode { - ChatTheme( - config = ChatUiConfig( - channelList = ChannelListConfig( - muteIndicatorPosition = MuteIndicatorPosition.TrailingBottom, - ), - ), - ) { - ChannelItemMutedTrailingBottom() - } + ChannelItemMutedTrailingBottom() + } + } + + @Test + fun `pinned channel`() { + snapshotWithDarkMode { + ChannelItemPinned() + } + } + + @Test + fun `pinned channel trailing bottom`() { + snapshotWithDarkMode { + ChannelItemPinnedTrailingBottom() + } + } + + @Test + fun `muted and pinned channel`() { + snapshotWithDarkMode { + ChannelItemMutedPinned() + } + } + + @Test + fun `muted and pinned channel trailing bottom`() { + snapshotWithDarkMode { + ChannelItemMutedPinnedTrailingBottom() + } + } + + @Test + fun `muted and pinned channel mixed positions`() { + snapshotWithDarkMode { + ChannelItemMutedPinnedMixed() } } 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 693ce4d8d97..ec06ac36a78 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 @@ -16,19 +16,12 @@ package io.getstream.chat.android.compose.ui.channels -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import io.getstream.chat.android.compose.ui.PaparazziComposeTest -import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenu -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 io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenuBottomSheetDialog +import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenuCenteredDialog +import io.getstream.chat.android.compose.ui.channels.info.SelectedChannelMenuMutedPinned import org.junit.Rule import org.junit.Test @@ -37,45 +30,24 @@ internal class SelectedChannelMenuTest : PaparazziComposeTest { @get:Rule override val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_2) + @Test + fun `selected channel centered dialog`() { + snapshotWithDarkMode { + SelectedChannelMenuCenteredDialog() + } + } + @Test fun `selected channel`() { - snapshot { - val channel = PreviewChannelData.channelWithManyMembers.copy( - ownCapabilities = ChannelCapabilities.toSet(), - ) - Box(modifier = Modifier.fillMaxSize()) { - SelectedChannelMenu( - modifier = Modifier.align(Alignment.BottomCenter), - selectedChannel = channel, - currentUser = PreviewUserData.user1, - channelActions = listOf( - ViewInfo(channel = channel, label = "Channel Info", onAction = {}), - ), - onChannelOptionConfirm = {}, - onDismiss = {}, - ) - } + snapshotWithDarkMode { + SelectedChannelMenuBottomSheetDialog() } } @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 = channel, - currentUser = PreviewUserData.user1, - channelActions = listOf( - ViewInfo(channel = channel, label = "Channel Info", onAction = {}), - ), - onChannelOptionConfirm = {}, - onDismiss = {}, - ) - } + fun `selected channel muted and pinned`() { + snapshotWithDarkMode { + SelectedChannelMenuMutedPinned() } } } diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/ChannelUtilsTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/ChannelUtilsTest.kt new file mode 100644 index 00000000000..07f5a3ee1cd --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/ChannelUtilsTest.kt @@ -0,0 +1,93 @@ +/* + * 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.util + +import io.getstream.chat.android.models.User +import io.getstream.chat.android.randomChannel +import io.getstream.chat.android.randomMember +import io.getstream.chat.android.randomUser +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +internal class ChannelUtilsTest { + + @Test + fun `Given a DM with current user and counterpart Should return the counterpart id`() { + val currentUser = randomUser() + val counterpart = randomUser() + val channel = dmChannel(currentUser, counterpart) + + val id = channel.dmCounterpartId(currentUser) + + assertEquals(counterpart.id, id) + } + + @Test + fun `Given a non-distinct channel Should return null`() { + val currentUser = randomUser() + val counterpart = randomUser() + val channel = randomChannel( + id = "regular-id", + type = "messaging", + members = listOf(randomMember(user = currentUser), randomMember(user = counterpart)), + ) + + assertNull(channel.dmCounterpartId(currentUser)) + } + + @Test + fun `Given a distinct channel with three members Should return null`() { + val currentUser = randomUser() + val channel = randomChannel( + id = "!members-${currentUser.id}-a-b", + type = "messaging", + members = listOf( + randomMember(user = currentUser), + randomMember(user = randomUser()), + randomMember(user = randomUser()), + ), + ) + + assertNull(channel.dmCounterpartId(currentUser)) + } + + @Test + fun `Given a distinct two-member channel without the current user Should return null`() { + val currentUser = randomUser() + val channel = randomChannel( + id = "!members-a-b", + type = "messaging", + members = listOf(randomMember(user = randomUser()), randomMember(user = randomUser())), + ) + + assertNull(channel.dmCounterpartId(currentUser)) + } + + @Test + fun `Given a null current user Should return null`() { + val channel = dmChannel(randomUser(), randomUser()) + + assertNull(channel.dmCounterpartId(currentUser = null)) + } + + private fun dmChannel(currentUser: User, counterpart: User) = randomChannel( + id = "!members-${currentUser.id}-${counterpart.id}", + type = "messaging", + members = listOf(randomMember(user = currentUser), randomMember(user = counterpart)), + ) +} diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel.png new file mode 100644 index 00000000000..a4f6cc0ba41 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_mixed_positions.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_mixed_positions.png new file mode 100644 index 00000000000..756a89d10ed Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_mixed_positions.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_trailing_bottom.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_trailing_bottom.png new file mode 100644 index 00000000000..119491c4b15 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_and_pinned_channel_trailing_bottom.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 index ff34b3c3db0..0ca1619a916 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_muted_channel_trailing_bottom.png 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_pinned_channel.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_pinned_channel.png new file mode 100644 index 00000000000..6bd16b13995 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_pinned_channel.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_pinned_channel_trailing_bottom.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_pinned_channel_trailing_bottom.png new file mode 100644 index 00000000000..6fcfc9dd7cc Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_ChannelItemTest_pinned_channel_trailing_bottom.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 e87f4e214c3..023756182ce 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_centered_dialog.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_centered_dialog.png new file mode 100644 index 00000000000..3689ba9a35b Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_centered_dialog.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 deleted file mode 100644 index 98b9210cc18..00000000000 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 /dev/null differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_muted_and_pinned.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_muted_and_pinned.png new file mode 100644 index 00000000000..ba734f46540 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channels_SelectedChannelMenuTest_selected_channel_muted_and_pinned.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 061d1250e92..832200930a3 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 @@ -46,7 +46,6 @@ private object SelectedChannelMenuUsageSnippet { if (currentlySelectedChannel != null) { val channelActions = buildDefaultChannelActions( selectedChannel = currentlySelectedChannel, - isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), ownCapabilities = currentlySelectedChannel.ownCapabilities, viewModel = listViewModel, onViewInfoAction = {}, @@ -94,7 +93,6 @@ private object SelectedChannelMenuHandlingActionsSnippet { if (currentlySelectedChannel != null) { val channelActions = buildDefaultChannelActions( selectedChannel = currentlySelectedChannel, - isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), ownCapabilities = currentlySelectedChannel.ownCapabilities, viewModel = listViewModel, onViewInfoAction = { channel -> @@ -146,7 +144,6 @@ private object SelectedChannelMenuCustomizationSnippet { if (currentlySelectedChannel != null) { val channelActions = buildDefaultChannelActions( selectedChannel = currentlySelectedChannel, - isMuted = listViewModel.isChannelMuted(currentlySelectedChannel.cid), ownCapabilities = currentlySelectedChannel.ownCapabilities, viewModel = listViewModel, onViewInfoAction = {},