diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index f3869d3cd81..e96358e1465 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -271,7 +271,7 @@ class MessagesActivity : ComponentActivity() { selectedMessage = selectedMessage, currentUser = user, isInThread = listViewModel.isInThread, - ownCapabilities = selectedMessageState.ownCapabilities, + channel = selectedMessageState.channel, ), message = selectedMessage, ownCapabilities = selectedMessageState.ownCapabilities, 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..6d7d8b25f11 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1640,10 +1640,11 @@ public final class io/getstream/chat/android/compose/ui/components/composer/Mess public final class io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility { public static final field $stable I public fun ()V - public fun (ZZZZZZZZZZ)V - public synthetic fun (ZZZZZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZZZZZZZZZ)V + public synthetic fun (ZZZZZZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component10 ()Z + public final fun component11 ()Z public final fun component2 ()Z public final fun component3 ()Z public final fun component4 ()Z @@ -1652,8 +1653,8 @@ public final class io/getstream/chat/android/compose/ui/components/messageoption public final fun component7 ()Z public final fun component8 ()Z public final fun component9 ()Z - public final fun copy (ZZZZZZZZZZ)Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility;ZZZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility; + public final fun copy (ZZZZZZZZZZZ)Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility;ZZZZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun isBlockUserVisible ()Z @@ -1662,6 +1663,7 @@ public final class io/getstream/chat/android/compose/ui/components/messageoption public final fun isEditMessageVisible ()Z public final fun isFlagMessageVisible ()Z public final fun isMarkAsUnreadVisible ()Z + public final fun isMuteUserVisible ()Z public final fun isPinMessageVisible ()Z public final fun isReplyVisible ()Z public final fun isRetryMessageVisible ()Z @@ -1671,7 +1673,7 @@ public final class io/getstream/chat/android/compose/ui/components/messageoption public final class io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionsKt { public static final fun MessageOptions (Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V - public static final fun defaultMessageOptionsState (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZLjava/util/Set;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun defaultMessageOptionsState (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZLio/getstream/chat/android/models/Channel;Landroidx/compose/runtime/Composer;I)Ljava/util/List; } public final class io/getstream/chat/android/compose/ui/components/messages/ComposableSingletons$MessageReactionsKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility.kt index bbdb9f4dea2..4e258aedcb9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptionItemVisibility.kt @@ -30,6 +30,7 @@ import io.getstream.chat.android.compose.ui.components.selectedmessage.SelectedM * @param isFlagMessageVisible Visibility of the flag message option. * @param isPinMessageVisible Visibility of the pin message to chat option. * @param isDeleteMessageVisible Visibility of the delete message option. + * @param isMuteUserVisible Visibility of the mute user option. * @param isBlockUserVisible Visibility of the block user option. * * @see [SelectedMessageMenu] @@ -46,5 +47,6 @@ public data class MessageOptionItemVisibility( val isFlagMessageVisible: Boolean = true, val isPinMessageVisible: Boolean = true, val isDeleteMessageVisible: Boolean = true, + val isMuteUserVisible: Boolean = true, val isBlockUserVisible: Boolean = true, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptions.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptions.kt index 4d1d34811b2..555c0ece653 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptions.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messageoptions/MessageOptions.kt @@ -30,11 +30,13 @@ import io.getstream.chat.android.compose.util.extensions.canDeleteMessage import io.getstream.chat.android.compose.util.extensions.canEditMessage import io.getstream.chat.android.compose.util.extensions.canFlagMessage import io.getstream.chat.android.compose.util.extensions.canMarkAsUnread +import io.getstream.chat.android.compose.util.extensions.canMuteUser import io.getstream.chat.android.compose.util.extensions.canPinMessage import io.getstream.chat.android.compose.util.extensions.canReplyToMessage import io.getstream.chat.android.compose.util.extensions.canRetryMessage import io.getstream.chat.android.compose.util.extensions.canThreadReplyToMessage import io.getstream.chat.android.compose.util.extensions.toSet +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus @@ -47,11 +49,13 @@ import io.getstream.chat.android.ui.common.state.messages.Delete import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.Flag import io.getstream.chat.android.ui.common.state.messages.MarkAsUnread +import io.getstream.chat.android.ui.common.state.messages.MuteUser import io.getstream.chat.android.ui.common.state.messages.Pin import io.getstream.chat.android.ui.common.state.messages.Reply import io.getstream.chat.android.ui.common.state.messages.Resend import io.getstream.chat.android.ui.common.state.messages.ThreadReply import io.getstream.chat.android.ui.common.state.messages.UnblockUser +import io.getstream.chat.android.ui.common.state.messages.UnmuteUser /** * Displays all [MessageOptionItemState]s. @@ -83,8 +87,7 @@ public fun MessageOptions( * @param selectedMessage Currently selected message, used to callbacks. * @param currentUser Current user, used to expose different states for messages. * @param isInThread If the message is being displayed in a thread. - * @param ownCapabilities Set of capabilities the user is given for the current channel. - * For a full list @see [ChannelCapabilities]. + * @param channel The channel where the message was sent. */ @Suppress("LongMethod") @Composable @@ -92,12 +95,13 @@ public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, - ownCapabilities: Set, + channel: Channel, ): List { if (selectedMessage.id.isEmpty()) { return emptyList() } val selectedMessageUserId = selectedMessage.user.id + val ownCapabilities = channel.ownCapabilities val visibility = ChatTheme.messageOptionsTheme.optionVisibility return listOfNotNull( @@ -187,6 +191,19 @@ public fun defaultMessageOptionsState( } else { null }, + if (visibility.canMuteUser(currentUser, selectedMessage, channel)) { + val isSenderMuted = currentUser?.mutes?.any { it.target?.id == selectedMessageUserId } == true + MessageOptionItemState( + title = if (isSenderMuted) R.string.stream_compose_unmute_user else R.string.stream_compose_mute_user, + iconPainter = painterResource( + if (isSenderMuted) R.drawable.stream_compose_ic_unmute else R.drawable.stream_compose_ic_mute, + ), + action = if (isSenderMuted) UnmuteUser(selectedMessage) else MuteUser(selectedMessage), + destructive = false, + ) + } else { + null + }, if (visibility.canBlockUser(currentUser, selectedMessage)) { val isSenderBlocked = currentUser?.blockedUserIds?.contains(selectedMessageUserId) == true val title = if (isSenderBlocked) { @@ -283,7 +300,7 @@ private fun MessageOptionsPreview( selectedMessage = selectedMMessage, currentUser = currentUser, isInThread = false, - ownCapabilities = ChannelCapabilities.toSet(), + channel = Channel(ownCapabilities = ChannelCapabilities.toSet()), ) MessageOptions(options = messageOptionsStateList, onMessageOptionSelected = {}) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt index 763dcb20566..4b889fe5c98 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/selectedmessage/SelectedMessageMenu.kt @@ -62,6 +62,7 @@ import io.getstream.chat.android.compose.ui.messages.list.LocalSelectedMessageBo import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.util.extensions.toSet +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.ReactionSortingByLastReactionAt @@ -311,7 +312,7 @@ private fun SelectedMessageMenuPreview(selectedMessage: Message) { selectedMessage = selectedMessage, currentUser = PreviewUserData.user1, isInThread = false, - ownCapabilities = ChannelCapabilities.toSet(), + channel = Channel(ownCapabilities = ChannelCapabilities.toSet()), ) SelectedMessageMenu( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index 73dbb8edd38..e0ee5be3842 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -441,7 +441,8 @@ private fun BoxScope.MessagesScreenMenus( ) { val user by listViewModel.user.collectAsState() - val ownCapabilities = selectedMessageState?.ownCapabilities ?: setOf() + val channel = selectedMessageState?.channel ?: Channel() + val ownCapabilities = channel.ownCapabilities val isInThread = listViewModel.isInThread @@ -449,7 +450,7 @@ private fun BoxScope.MessagesScreenMenus( selectedMessage = selectedMessage, currentUser = user, isInThread = isInThread, - ownCapabilities = ownCapabilities, + channel = channel, ) var messageOptions by remember { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibility.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibility.kt index 663597d133d..29934898933 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibility.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibility.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.util.extensions import io.getstream.chat.android.compose.ui.components.messageoptions.MessageOptionItemVisibility +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.utils.canBlockUser @@ -25,6 +26,7 @@ import io.getstream.chat.android.ui.common.utils.canDeleteMessage import io.getstream.chat.android.ui.common.utils.canEditMessage import io.getstream.chat.android.ui.common.utils.canFlagMessage import io.getstream.chat.android.ui.common.utils.canMarkAsUnread +import io.getstream.chat.android.ui.common.utils.canMuteUser import io.getstream.chat.android.ui.common.utils.canPinMessage import io.getstream.chat.android.ui.common.utils.canReplyToMessage import io.getstream.chat.android.ui.common.utils.canRetryMessage @@ -99,6 +101,17 @@ internal fun MessageOptionItemVisibility.canPinMessage( ownCapabilities = ownCapabilities, ) +internal fun MessageOptionItemVisibility.canMuteUser( + currentUser: User?, + message: Message, + channel: Channel, +): Boolean = canMuteUser( + muteUserEnabled = isMuteUserVisible, + currentUser = currentUser, + message = message, + channel = channel, +) + internal fun MessageOptionItemVisibility.canBlockUser( currentUser: User?, message: Message, diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt index efb7696a179..e2c0f46f350 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt @@ -18,7 +18,9 @@ package io.getstream.chat.android.compose.util.extensions import io.getstream.chat.android.compose.ui.components.messageoptions.MessageOptionItemVisibility import io.getstream.chat.android.models.AttachmentType +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.models.Config import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus import io.getstream.chat.android.models.User @@ -119,6 +121,18 @@ internal class MessageOptionItemVisibilityTest { messageOptionItemVisibility.canPinMessage(message, ownCapabilities) `should be` expectedResult } + @ParameterizedTest + @MethodSource("canMuteUserArguments") + fun `Verify canMuteUser() extension function returns proper value`( + messageOptionItemVisibility: MessageOptionItemVisibility, + currentUser: User?, + message: Message, + channel: Channel, + expectedResult: Boolean, + ) { + messageOptionItemVisibility.canMuteUser(currentUser, message, channel) `should be` expectedResult + } + @ParameterizedTest @MethodSource("canBlockUserArguments") fun `Verify canBlockUser() extension function return proper value`( @@ -284,6 +298,38 @@ internal class MessageOptionItemVisibilityTest { ), ) + @JvmStatic + fun canMuteUserArguments() = listOf( + Arguments.of( + MessageOptionItemVisibility(isMuteUserVisible = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = true)), + false, + ), + Arguments.of( + MessageOptionItemVisibility(), + currentUser, + randomMessage(user = currentUser), + Channel(config = Config(muteEnabled = true)), + false, + ), + Arguments.of( + MessageOptionItemVisibility(isMuteUserVisible = true), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = false)), + false, + ), + Arguments.of( + MessageOptionItemVisibility(isMuteUserVisible = true), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = true)), + true, + ), + ) + @JvmStatic fun canBlockUserArguments() = listOf( Arguments.of( diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message.png index d31027c06f2..ffe85131877 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message_in_dark_mode.png index b92742b1f1e..87487de77b9 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_SelectedMessageMenuTest_their_message_in_dark_mode.png differ diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/SelectedMessageMenu.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/SelectedMessageMenu.kt index 6fe496a42e2..2ef207912d3 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/SelectedMessageMenu.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/SelectedMessageMenu.kt @@ -59,7 +59,7 @@ private object SelectedMessageMenuUsageSnippet { selectedMessage = selectedMessage, currentUser = user, isInThread = listViewModel.isInThread, - ownCapabilities = selectedMessageState.ownCapabilities + channel = selectedMessageState.channel ), // The message you selected message = selectedMessage, @@ -121,7 +121,7 @@ private object SelectedMessageMenuHandlingActionsSnippet { selectedMessage = selectedMessage, currentUser = user, isInThread = listViewModel.isInThread, - ownCapabilities = selectedMessageState.ownCapabilities + channel = selectedMessageState.channel ), // The message you selected message = selectedMessage, @@ -184,7 +184,7 @@ private object SelectedMessageMenuCustomizationSnippet { selectedMessage = selectedMessage, currentUser = user, isInThread = listViewModel.isInThread, - ownCapabilities = selectedMessageState.ownCapabilities + channel = selectedMessageState.channel ), // The capabilities the user has in a given channel ownCapabilities = selectedMessageState.ownCapabilities, 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..d7e7fbccb7c 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 @@ -1981,6 +1981,18 @@ public final class io/getstream/chat/android/ui/common/state/messages/MessageMod public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/state/messages/MuteUser : io/getstream/chat/android/ui/common/state/messages/MessageAction { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/Message;)V + public final fun component1 ()Lio/getstream/chat/android/models/Message; + public final fun copy (Lio/getstream/chat/android/models/Message;)Lio/getstream/chat/android/ui/common/state/messages/MuteUser; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/MuteUser;Lio/getstream/chat/android/models/Message;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/MuteUser; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Lio/getstream/chat/android/models/Message; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/state/messages/Pin : io/getstream/chat/android/ui/common/state/messages/MessageAction { public static final field $stable I public fun (Lio/getstream/chat/android/models/Message;)V @@ -2058,6 +2070,18 @@ public final class io/getstream/chat/android/ui/common/state/messages/UnblockUse public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/state/messages/UnmuteUser : io/getstream/chat/android/ui/common/state/messages/MessageAction { + public static final field $stable I + public fun (Lio/getstream/chat/android/models/Message;)V + public final fun component1 ()Lio/getstream/chat/android/models/Message; + public final fun copy (Lio/getstream/chat/android/models/Message;)Lio/getstream/chat/android/ui/common/state/messages/UnmuteUser; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/UnmuteUser;Lio/getstream/chat/android/models/Message;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/UnmuteUser; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Lio/getstream/chat/android/models/Message; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/state/messages/composer/AttachmentMetaData { public static final field $stable I public fun ()V @@ -2650,64 +2674,65 @@ public final class io/getstream/chat/android/ui/common/state/messages/list/Other public final class io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState : io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; - public final fun component2 ()Ljava/util/Set; - public final fun copy (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState;Lio/getstream/chat/android/models/Message;Ljava/util/Set;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState; + public final fun component2 ()Lio/getstream/chat/android/models/Channel; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageFailedModerationState; public fun equals (Ljava/lang/Object;)Z + public fun getChannel ()Lio/getstream/chat/android/models/Channel; public fun getMessage ()Lio/getstream/chat/android/models/Message; - public fun getOwnCapabilities ()Ljava/util/Set; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState : io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; - public final fun component2 ()Ljava/util/Set; - public final fun copy (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState;Lio/getstream/chat/android/models/Message;Ljava/util/Set;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState; + public final fun component2 ()Lio/getstream/chat/android/models/Channel; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageOptionsState; public fun equals (Ljava/lang/Object;)Z + public fun getChannel ()Lio/getstream/chat/android/models/Channel; public fun getMessage ()Lio/getstream/chat/android/models/Message; - public fun getOwnCapabilities ()Ljava/util/Set; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState : io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; - public final fun component2 ()Ljava/util/Set; - public final fun copy (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState;Lio/getstream/chat/android/models/Message;Ljava/util/Set;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState; + public final fun component2 ()Lio/getstream/chat/android/models/Channel; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsPickerState; public fun equals (Ljava/lang/Object;)Z + public fun getChannel ()Lio/getstream/chat/android/models/Channel; public fun getMessage ()Lio/getstream/chat/android/models/Message; - public fun getOwnCapabilities ()Ljava/util/Set; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState : io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)V + public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; - public final fun component2 ()Ljava/util/Set; - public final fun copy (Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState;Lio/getstream/chat/android/models/Message;Ljava/util/Set;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState; + public final fun component2 ()Lio/getstream/chat/android/models/Channel; + public final fun copy (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/ui/common/state/messages/list/SelectedMessageReactionsState; public fun equals (Ljava/lang/Object;)Z + public fun getChannel ()Lio/getstream/chat/android/models/Channel; public fun getMessage ()Lio/getstream/chat/android/models/Message; - public fun getOwnCapabilities ()Ljava/util/Set; public fun hashCode ()I public fun toString ()Ljava/lang/String; } public abstract class io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState { public static final field $stable I + public abstract fun getChannel ()Lio/getstream/chat/android/models/Channel; public abstract fun getMessage ()Lio/getstream/chat/android/models/Message; - public abstract fun getOwnCapabilities ()Ljava/util/Set; + public final fun getOwnCapabilities ()Ljava/util/Set; } public final class io/getstream/chat/android/ui/common/state/messages/list/SendAnyway : io/getstream/chat/android/ui/common/state/messages/list/ModeratedMessageOption { @@ -2949,6 +2974,7 @@ public final class io/getstream/chat/android/ui/common/utils/CapabilitiesHelperK public static final fun canEditMessage (ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Z public static final fun canFlagMessage (ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/Message;Ljava/util/Set;)Z public static final fun canMarkAsUnread (ZLjava/util/Set;)Z + public static final fun canMuteUser (ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Channel;)Z public static final fun canPinMessage (ZLio/getstream/chat/android/models/Message;Ljava/util/Set;)Z public static final fun canReplyToMessage (ZLio/getstream/chat/android/models/Message;Ljava/util/Set;)Z public static final fun canRetryMessage (ZLio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/Message;)Z diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index f688a38df9d..8ecdc3428f2 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -75,12 +75,14 @@ import io.getstream.chat.android.ui.common.state.messages.Delete import io.getstream.chat.android.ui.common.state.messages.MarkAsUnread import io.getstream.chat.android.ui.common.state.messages.MessageAction import io.getstream.chat.android.ui.common.state.messages.MessageMode +import io.getstream.chat.android.ui.common.state.messages.MuteUser import io.getstream.chat.android.ui.common.state.messages.Pin import io.getstream.chat.android.ui.common.state.messages.React import io.getstream.chat.android.ui.common.state.messages.Reply import io.getstream.chat.android.ui.common.state.messages.Resend import io.getstream.chat.android.ui.common.state.messages.ThreadReply import io.getstream.chat.android.ui.common.state.messages.UnblockUser +import io.getstream.chat.android.ui.common.state.messages.UnmuteUser import io.getstream.chat.android.ui.common.state.messages.list.CancelGiphy import io.getstream.chat.android.ui.common.state.messages.list.DateSeparatorItemState import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility @@ -1508,12 +1510,12 @@ public class MessageListController( if (it.isModerationError(currentUserId)) { SelectedMessageFailedModerationState( message = it, - ownCapabilities = ownCapabilities.value, + channel = channel.value, ) } else { SelectedMessageOptionsState( message = it, - ownCapabilities = ownCapabilities.value, + channel = channel.value, ) } }, @@ -1530,7 +1532,7 @@ public class MessageListController( changeSelectMessageState( SelectedMessageReactionsState( message = message, - ownCapabilities = ownCapabilities.value, + channel = channel.value, ), ) } @@ -1546,7 +1548,7 @@ public class MessageListController( changeSelectMessageState( SelectedMessageReactionsPickerState( message = message, - ownCapabilities = ownCapabilities.value, + channel = channel.value, ), ) } @@ -1613,6 +1615,8 @@ public class MessageListController( _messageActions.value = _messageActions.value + messageAction } + is MuteUser -> muteUser(messageAction.message.user) + is UnmuteUser -> unmuteUser(messageAction.message.user) is BlockUser -> blockUser(messageAction.message.user.id) is UnblockUser -> unblockUser(messageAction.message.user.id) is Copy -> copyMessage(messageAction.message) diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageAction.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageAction.kt index 5cf47d037aa..08873c22347 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageAction.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageAction.kt @@ -100,6 +100,16 @@ public data class Flag( override val message: Message, ) : MessageAction() +/** + * Mute the sender of the message. + */ +public data class MuteUser(override val message: Message) : MessageAction() + +/** + * Unmute the sender of the message. + */ +public data class UnmuteUser(override val message: Message) : MessageAction() + /** * Block the sender of the message. */ @@ -133,6 +143,8 @@ public fun MessageAction.updateMessage(message: Message): MessageAction { is Delete -> copy(message = message) is Flag -> copy(message = message) is CustomAction -> copy(message = message) + is MuteUser -> copy(message = message) + is UnmuteUser -> copy(message = message) is BlockUser -> copy(message = message) is UnblockUser -> copy(message = message) } diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState.kt index 27296cd6798..d4d96a437ce 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/SelectedMessageState.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.ui.common.state.messages.list +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Message @@ -23,12 +24,13 @@ import io.getstream.chat.android.models.Message * Represents a state when a message or its reactions were selected. * * @property message The selected message. - * @property ownCapabilities Set of capabilities the user is given for the current channel. - * For a full list @see [ChannelCapabilities]. + * @property channel The channel where the message was selected. + * For a full list of capabilities @see [ChannelCapabilities]. */ public sealed class SelectedMessageState { public abstract val message: Message - public abstract val ownCapabilities: Set + public abstract val channel: Channel + public val ownCapabilities: Set get() = channel.ownCapabilities } /** @@ -36,7 +38,7 @@ public sealed class SelectedMessageState { */ public data class SelectedMessageOptionsState( override val message: Message, - override val ownCapabilities: Set, + override val channel: Channel, ) : SelectedMessageState() /** @@ -44,7 +46,7 @@ public data class SelectedMessageOptionsState( */ public data class SelectedMessageReactionsState( override val message: Message, - override val ownCapabilities: Set, + override val channel: Channel, ) : SelectedMessageState() /** @@ -52,7 +54,7 @@ public data class SelectedMessageReactionsState( */ public data class SelectedMessageReactionsPickerState( override val message: Message, - override val ownCapabilities: Set, + override val channel: Channel, ) : SelectedMessageState() /** @@ -60,5 +62,5 @@ public data class SelectedMessageReactionsPickerState( */ public data class SelectedMessageFailedModerationState( override val message: Message, - override val ownCapabilities: Set, + override val channel: Channel, ) : SelectedMessageState() diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt index 8d0c4103911..7bbb4fe93cb 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt @@ -22,6 +22,7 @@ import io.getstream.chat.android.client.utils.attachment.isGiphy import io.getstream.chat.android.client.utils.message.hasSharedLocation import io.getstream.chat.android.client.utils.message.isThreadReply import io.getstream.chat.android.models.AttachmentType +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus @@ -190,6 +191,27 @@ public fun canPinMessage( ownCapabilities: Set, ): Boolean = pinMessageEnabled && message.isSynced() && ownCapabilities.canPinMessage() +/** + * Determines whether the user who sent the given message can be muted. + * + * A user can be muted when: + * - Mute user functionality is enabled in the UI configuration + * - Muting is enabled in the channel configuration + * - The message was not sent by the current user (users cannot mute themselves) + * + * @param muteUserEnabled Whether the mute user feature is enabled in the UI. + * @param currentUser The currently authenticated user. + * @param message The message whose sender to check for mute eligibility. + * @param channel The channel where the message was sent. + * @return `true` if the message sender can be muted, `false` otherwise. + */ +public fun canMuteUser( + muteUserEnabled: Boolean, + currentUser: User?, + message: Message, + channel: Channel, +): Boolean = muteUserEnabled && channel.config.muteEnabled && !message.isOwnMessage(currentUser) + /** * Determines whether the user who sent the given message can be blocked. * diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt index 736843285d8..b786f4baab0 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt @@ -17,7 +17,9 @@ package io.getstream.chat.android.ui.common.utils import io.getstream.chat.android.models.AttachmentType +import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.models.Config import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.SyncStatus import io.getstream.chat.android.models.User @@ -125,6 +127,18 @@ internal class CapabilitiesHelperTest { canBlockUser(localFlag, currentUser, message) `should be` expectedResult } + @ParameterizedTest + @MethodSource("canMuteUserArguments") + fun `Verify canMuteUser() extension function returns proper value`( + localFlag: Boolean, + currentUser: User?, + message: Message, + channel: Channel, + expectedResult: Boolean, + ) { + canMuteUser(localFlag, currentUser, message, channel) `should be` expectedResult + } + @ParameterizedTest @MethodSource("canMarkAsUnreadArguments") fun `Verify canMarkAsUnread() extension function return proper value`( @@ -301,6 +315,42 @@ internal class CapabilitiesHelperTest { ), ) + @JvmStatic + fun canMuteUserArguments() = listOf( + // UI flag disabled + Arguments.of( + false, + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = true)), + false, + ), + // own message + Arguments.of( + randomBoolean(), + currentUser, + randomMessage(user = currentUser), + Channel(config = Config(muteEnabled = true)), + false, + ), + // mute disabled in channel config + Arguments.of( + true, + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = false)), + false, + ), + // all conditions met + Arguments.of( + true, + currentUser.takeIf { randomBoolean() }, + randomMessage(), + Channel(config = Config(muteEnabled = true)), + true, + ), + ) + @JvmStatic fun canRetryMessageArguments() = listOf( Arguments.of( diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt index 4b17d62615f..116578e8743 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt @@ -65,12 +65,14 @@ import io.getstream.chat.android.ui.common.state.messages.Delete import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MarkAsUnread import io.getstream.chat.android.ui.common.state.messages.MessageAction +import io.getstream.chat.android.ui.common.state.messages.MuteUser import io.getstream.chat.android.ui.common.state.messages.Pin import io.getstream.chat.android.ui.common.state.messages.React import io.getstream.chat.android.ui.common.state.messages.Reply import io.getstream.chat.android.ui.common.state.messages.Resend import io.getstream.chat.android.ui.common.state.messages.ThreadReply import io.getstream.chat.android.ui.common.state.messages.UnblockUser +import io.getstream.chat.android.ui.common.state.messages.UnmuteUser import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility import io.getstream.chat.android.ui.common.state.messages.list.GiphyAction import io.getstream.chat.android.ui.common.state.messages.list.ModeratedMessageOption @@ -2252,6 +2254,11 @@ public class MessageListView : ConstraintLayout { is UnblockUser -> { messageUserUnblockHandler.onUserUnblocked(message) } + is MuteUser, + is UnmuteUser, + -> { + // Not displayed in the XML UI + } } }