diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt index 556aacf7cb8..c5eaadce1de 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt @@ -45,12 +45,14 @@ import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.FilterObject import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Mute import io.getstream.chat.android.models.TypingEvent import io.getstream.chat.android.models.User import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.models.querysort.QuerySorter import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction import io.getstream.chat.android.ui.common.utils.extensions.defaultChannelListFilter +import io.getstream.chat.android.ui.common.utils.extensions.isOneToOne import io.getstream.log.taggedLogger import io.getstream.result.call.toUnitCall import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -200,7 +202,7 @@ public class ChannelListViewModel( .flatMapLatest { it.channelMutes } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - private val globalMuted: StateFlow> = globalState + private val globalMuted: StateFlow> = globalState .flatMapLatest { it.muted } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -432,7 +434,8 @@ public class ChannelListViewModel( channelMutes, typingChannels, channelDraftMessages, - ) { state, channelMutes, typingChannels, channelDraftMessages -> + globalMuted, + ) { state, channelMutes, typingChannels, channelDraftMessages, userMutes -> when (state) { ChannelsStateData.NoQueryActive, ChannelsStateData.Loading, @@ -457,6 +460,8 @@ public class ChannelListViewModel( channelItems = createChannelItems( channels = state.channels, channelMutes = channelMutes, + userMutes = userMutes, + currentUser = user.value, typingEvents = typingChannels, draftMessages = channelDraftMessages.takeIf { isDraftMessageEnabled } ?: emptyMap(), ), @@ -800,25 +805,41 @@ public class ChannelListViewModel( * * @param channels The channels to show. * @param channelMutes The list of channels muted for the current user. - * + * @param userMutes The list of users muted by the current user. + * @param currentUser The currently logged in user. */ + @Suppress("LongParameterList") private fun createChannelItems( channels: List, channelMutes: List, + userMutes: List, + currentUser: User?, typingEvents: Map, draftMessages: Map, ): List { val mutedChannelIds = channelMutes.map { channelMute -> channelMute.channel?.cid }.toSet() + val mutedUserIds = userMutes.mapNotNullTo(mutableSetOf()) { it.target?.id } return channels.map { ItemState.ChannelItemState( channel = it, - isMuted = it.cid in mutedChannelIds, + isMuted = it.cid in mutedChannelIds || it.isOneToOneMutedByUser(currentUser, mutedUserIds), typingUsers = typingEvents[it.cid]?.users ?: emptyList(), draftMessage = draftMessages[it.cid], ) } } + /** + * Checks if a 1:1 channel is muted via user mute (i.e. the other member is muted). + */ + private fun Channel.isOneToOneMutedByUser(currentUser: User?, mutedUserIds: Set) = + if (mutedUserIds.isEmpty() || currentUser == null || !isOneToOne(currentUser)) { + false + } else { + val otherUser = members.find { it.user.id != currentUser.id }?.user + otherUser != null && otherUser.id in mutedUserIds + } + internal companion object { /** * Default value of number of channels to return when querying channels. diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt index 151364f9cd1..05f1d044ce0 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt @@ -36,7 +36,9 @@ import io.getstream.chat.android.models.ChannelMute import io.getstream.chat.android.models.FilterObject import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.InitializationState +import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Mute import io.getstream.chat.android.models.OrFilterObject import io.getstream.chat.android.models.SearchMessagesResult import io.getstream.chat.android.models.TypingEvent @@ -191,6 +193,68 @@ internal class ChannelListViewModelTest { verify(chatClient).unmuteChannel("messaging", "channel1") } + @Test + fun `Given a DM with a muted user Should mark the channel as muted`() = runTest { + val viewModel = Fixture() + .givenCurrentUser(currentUser) + .givenChannelsQuery() + .givenChannelsState( + channelsStateData = ChannelsStateData.Result(listOf(directChannel)), + loading = false, + ) + .givenChannelMutes() + .givenUserMutes(listOf(otherUserMute)) + .givenTypingChannels() + .get(this) + + val channelItem = viewModel.channelsState.channelItems.first() as ItemState.ChannelItemState + assertTrue(channelItem.isMuted) + } + + @Test + fun `Given a DM without a muted user Should not mark the channel as muted`() = runTest { + val viewModel = Fixture() + .givenCurrentUser(currentUser) + .givenChannelsQuery() + .givenChannelsState( + channelsStateData = ChannelsStateData.Result(listOf(directChannel)), + loading = false, + ) + .givenChannelMutes() + .givenTypingChannels() + .get(this) + + val channelItem = viewModel.channelsState.channelItems.first() as ItemState.ChannelItemState + assertFalse(channelItem.isMuted) + } + + @Test + fun `Given a group channel with a muted user Should not mark the channel as muted`() = runTest { + val groupChannel = Channel( + type = "messaging", + id = "groupChannel", + members = listOf( + Member(user = currentUser), + Member(user = otherUser), + Member(user = User(id = "thirdUser")), + ), + ) + val viewModel = Fixture() + .givenCurrentUser(currentUser) + .givenChannelsQuery() + .givenChannelsState( + channelsStateData = ChannelsStateData.Result(listOf(groupChannel)), + loading = false, + ) + .givenChannelMutes() + .givenUserMutes(listOf(otherUserMute)) + .givenTypingChannels() + .get(this) + + val channelItem = viewModel.channelsState.channelItems.first() as ItemState.ChannelItemState + assertFalse(channelItem.isMuted) + } + @Test fun `Given channel list in content state When selecting a channel and dismissing the menu Should hide the menu`() = runTest { @@ -569,6 +633,10 @@ internal class ChannelListViewModelTest { whenever(globalState.channelMutes) doReturn MutableStateFlow(channelMutes) } + fun givenUserMutes(userMutes: List = emptyList()) = apply { + whenever(globalState.muted) doReturn MutableStateFlow(userMutes) + } + fun givenTypingChannels(typingChannels: Map = emptyMap()) = apply { whenever(globalState.typingChannels) doReturn MutableStateFlow(typingChannels) } @@ -649,6 +717,9 @@ internal class ChannelListViewModelTest { ) private val querySort = QuerySortByField.descByName("lastUpdated") + private val currentUser = User(id = "currentUser") + private val otherUser = User(id = "otherUser") + private val channel1: Channel = Channel( type = "messaging", id = "channel1", @@ -657,5 +728,20 @@ internal class ChannelListViewModelTest { type = "messaging", id = "channel2", ) + private val directChannel = Channel( + type = "messaging", + id = "!members-currentUser-otherUser", + members = listOf( + Member(user = currentUser), + Member(user = otherUser), + ), + ) + private val otherUserMute = Mute( + user = currentUser, + target = otherUser, + createdAt = Date(), + updatedAt = Date(), + expires = null, + ) } }