Skip to content

Commit a264e34

Browse files
authored
Update actions for channels in the channel list (#6327)
* Remove archive action from channel swipe actions * Remove ChannelAction.requiredCapability * Disable archive channel action by default * Revert showing muted icon on DM channels with muted users * Remove unmute group from channel actions sheet * Only show mute action as primary swipe action * Remove unused strings * Add flag for controlling mute user action independently from mute channel * Fix flaky channel preview e2e test by waiting for expected text
1 parent b9110c3 commit a264e34

File tree

10 files changed

+34
-256
lines changed

10 files changed

+34
-256
lines changed

stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.getstream.chat.android.compose.robots
1818

1919
import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelList.Channel
2020
import io.getstream.chat.android.compose.uiautomator.isDisplayed
21+
import io.getstream.chat.android.compose.uiautomator.waitForText
2122
import io.getstream.chat.android.compose.uiautomator.waitToAppear
2223
import io.getstream.chat.android.compose.uiautomator.waitToDisappear
2324
import io.getstream.chat.android.e2e.test.robots.ParticipantRobot
@@ -36,7 +37,7 @@ fun UserRobot.assertMessageInChannelPreview(text: String, fromCurrentUser: Boole
3637
false -> "${ParticipantRobot.name}: $text"
3738
null -> text
3839
}
39-
assertEquals(expectedPreview, Channel.messagePreview.waitToAppear().text.trimEnd())
40+
assertEquals(expectedPreview, Channel.messagePreview.waitToAppear().waitForText(expectedPreview).text.trimEnd())
4041
return this
4142
}
4243

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,22 +1192,24 @@ public final class io/getstream/chat/android/compose/ui/components/channels/Chan
11921192
public final class io/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility {
11931193
public static final field $stable I
11941194
public fun <init> ()V
1195-
public fun <init> (ZZZZZZ)V
1196-
public synthetic fun <init> (ZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
1195+
public fun <init> (ZZZZZZZ)V
1196+
public synthetic fun <init> (ZZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
11971197
public final fun component1 ()Z
11981198
public final fun component2 ()Z
11991199
public final fun component3 ()Z
12001200
public final fun component4 ()Z
12011201
public final fun component5 ()Z
12021202
public final fun component6 ()Z
1203-
public final fun copy (ZZZZZZ)Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;
1204-
public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;ZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;
1203+
public final fun component7 ()Z
1204+
public final fun copy (ZZZZZZZ)Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;
1205+
public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;ZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility;
12051206
public fun equals (Ljava/lang/Object;)Z
12061207
public fun hashCode ()I
12071208
public final fun isArchiveChannelVisible ()Z
12081209
public final fun isDeleteChannelVisible ()Z
12091210
public final fun isLeaveChannelVisible ()Z
12101211
public final fun isMuteChannelVisible ()Z
1212+
public final fun isMuteUserVisible ()Z
12111213
public final fun isPinChannelVisible ()Z
12121214
public final fun isViewInfoVisible ()Z
12131215
public fun toString ()Ljava/lang/String;

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/DefaultChannelSwipeActions.kt

Lines changed: 15 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,28 @@
1616

1717
package io.getstream.chat.android.compose.ui.channels.list
1818

19-
import android.content.res.Resources
2019
import androidx.compose.runtime.Composable
2120
import androidx.compose.runtime.remember
2221
import androidx.compose.runtime.rememberCoroutineScope
2322
import androidx.compose.runtime.rememberUpdatedState
24-
import androidx.compose.ui.platform.LocalContext
23+
import androidx.compose.ui.platform.LocalResources
2524
import androidx.compose.ui.res.painterResource
26-
import io.getstream.chat.android.client.extensions.isArchive
27-
import io.getstream.chat.android.client.extensions.isPinned
2825
import io.getstream.chat.android.compose.R
2926
import io.getstream.chat.android.compose.state.channels.list.ItemState
30-
import io.getstream.chat.android.compose.ui.util.isDistinct
27+
import io.getstream.chat.android.compose.ui.theme.ChatTheme
3128
import io.getstream.chat.android.models.Channel
3229
import io.getstream.chat.android.models.ChannelCapabilities
33-
import io.getstream.chat.android.ui.common.state.channels.actions.ArchiveChannel
3430
import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction
3531
import io.getstream.chat.android.ui.common.state.channels.actions.MuteChannel
36-
import io.getstream.chat.android.ui.common.state.channels.actions.PinChannel
37-
import io.getstream.chat.android.ui.common.state.channels.actions.UnarchiveChannel
3832
import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteChannel
39-
import io.getstream.chat.android.ui.common.state.channels.actions.UnpinChannel
4033
import kotlinx.coroutines.launch
4134

4235
/**
4336
* Default swipe actions for a channel list item.
4437
*
4538
* Shows two actions:
4639
* - **More** (gray, left): Opens the channel options bottom sheet.
47-
* - **Primary action** (blue, right): Archive for DMs, Mute for groups — with fallback priority.
48-
*
49-
* The primary action is resolved via a priority list:
50-
* - DM: Archive → Mute → Pin
51-
* - Group: Mute → Archive → Pin
40+
* - **Primary action** (blue, right): Mute/Unmute channel.
5241
*
5342
* Each action is a self-executing [ChannelAction] that invokes its handler via
5443
* [LocalSwipeActionHandler].
@@ -66,7 +55,7 @@ public fun DefaultChannelSwipeActions(channelItem: ItemState.ChannelItemState) {
6655
if (moreHandler != null) {
6756
SwipeActionItem(
6857
icon = painterResource(R.drawable.stream_design_ic_more),
69-
label = LocalContext.current.resources.getString(R.string.stream_compose_swipe_action_more),
58+
label = LocalResources.current.getString(R.string.stream_compose_swipe_action_more),
7059
onClick = {
7160
scope.launch { coordinator?.closeAll() }
7261
moreHandler(channel)
@@ -84,83 +73,38 @@ public fun DefaultChannelSwipeActions(channelItem: ItemState.ChannelItemState) {
8473
SwipeActionItem(
8574
icon = painterResource(primaryAction.icon),
8675
label = primaryAction.label,
87-
onClick = { primaryAction.onAction() },
76+
onClick = primaryAction.onAction,
8877
style = SwipeActionStyle.Primary,
8978
)
9079
}
9180
}
9281

9382
/**
94-
* Resolves and remembers the primary swipe action based on channel type and capabilities.
95-
*
96-
* DM priority: Archive → Mute → Pin.
97-
* Group priority: Mute → Archive → Pin.
83+
* Resolves and remembers the primary swipe action (mute/unmute channel).
9884
*
99-
* Archive and Pin are always available (membership operations, no capability gate).
100-
* Mute requires [ChannelCapabilities.MUTE_CHANNEL].
85+
* Requires [ChannelCapabilities.MUTE_CHANNEL] and `isMuteChannelVisible` in the theme.
10186
*/
10287
@Composable
10388
private fun rememberPrimarySwipeAction(
10489
channel: Channel,
10590
isMuted: Boolean,
10691
handler: (ChannelAction) -> Unit,
10792
): ChannelAction? {
108-
val resources = LocalContext.current.resources
93+
val resources = LocalResources.current
10994
val handlerState = rememberUpdatedState(handler)
110-
val isPinned = channel.isPinned()
111-
val isArchived = channel.isArchive()
112-
val canMute = channel.ownCapabilities.contains(ChannelCapabilities.MUTE_CHANNEL)
113-
val isDM = channel.isDistinct() && channel.members.size == 2
95+
val canMute = ChatTheme.channelOptionsTheme.optionVisibility.isMuteChannelVisible &&
96+
channel.ownCapabilities.contains(ChannelCapabilities.MUTE_CHANNEL)
11497

115-
return remember(channel.cid, isMuted, isPinned, isArchived, canMute, isDM) {
98+
return remember(channel.cid, isMuted, canMute) {
99+
if (!canMute) return@remember null
116100
var resolved: ChannelAction? = null
117101
val onAction: () -> Unit = { resolved?.let { handlerState.value(it) } }
118102

119-
val archiveAction = archiveAction(channel, isArchived, resources, onAction)
120-
val muteAction = muteAction(channel, isMuted, canMute, resources, onAction)
121-
val pinAction = pinAction(channel, isPinned, resources, onAction)
122-
123-
val candidates: List<ChannelAction?> = if (isDM) {
124-
listOf(archiveAction, muteAction, pinAction)
103+
resolved = if (isMuted) {
104+
UnmuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unmute), onAction)
125105
} else {
126-
listOf(muteAction, archiveAction, pinAction)
106+
MuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_mute), onAction)
127107
}
128-
129-
resolved = candidates.firstOrNull { it != null }
130108
resolved
131109
}
132110
}
133-
134-
private fun archiveAction(
135-
channel: Channel,
136-
isArchived: Boolean,
137-
resources: Resources,
138-
onAction: () -> Unit,
139-
): ChannelAction = if (isArchived) {
140-
UnarchiveChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unarchive), onAction)
141-
} else {
142-
ArchiveChannel(channel, resources.getString(R.string.stream_compose_swipe_action_archive), onAction)
143-
}
144-
145-
private fun muteAction(
146-
channel: Channel,
147-
isMuted: Boolean,
148-
canMute: Boolean,
149-
resources: Resources,
150-
onAction: () -> Unit,
151-
): ChannelAction? = when {
152-
!canMute -> null
153-
isMuted -> UnmuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unmute), onAction)
154-
else -> MuteChannel(channel, resources.getString(R.string.stream_compose_swipe_action_mute), onAction)
155-
}
156-
157-
private fun pinAction(
158-
channel: Channel,
159-
isPinned: Boolean,
160-
resources: Resources,
161-
onAction: () -> Unit,
162-
): ChannelAction = if (isPinned) {
163-
UnpinChannel(channel, resources.getString(R.string.stream_compose_swipe_action_unpin), onAction)
164-
} else {
165-
PinChannel(channel, resources.getString(R.string.stream_compose_swipe_action_pin), onAction)
166-
}

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptionItemVisibility.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package io.getstream.chat.android.compose.ui.components.channels
2222
* @param isViewInfoVisible Visibility of the view channel info option.
2323
* @param isLeaveChannelVisible Visibility of the leave channel option.
2424
* @param isMuteChannelVisible Visibility of the mute channel option.
25+
* @param isMuteUserVisible Visibility of the mute user option (DM channels only).
2526
* @param isArchiveChannelVisible Visibility of the archive channel option.
2627
* @param isPinChannelVisible Visibility of the pin channel option.
2728
* @param isDeleteChannelVisible Visibility of the delete channel option.
@@ -32,7 +33,8 @@ public data class ChannelOptionItemVisibility(
3233
val isViewInfoVisible: Boolean = true,
3334
val isLeaveChannelVisible: Boolean = true,
3435
val isMuteChannelVisible: Boolean = true,
35-
val isArchiveChannelVisible: Boolean = true,
36+
val isMuteUserVisible: Boolean = true,
37+
val isArchiveChannelVisible: Boolean = false,
3638
val isPinChannelVisible: Boolean = false,
3739
val isDeleteChannelVisible: Boolean = true,
3840
)

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/ChannelOptions.kt

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import io.getstream.chat.android.ui.common.state.channels.actions.MuteUser
5050
import io.getstream.chat.android.ui.common.state.channels.actions.PinChannel
5151
import io.getstream.chat.android.ui.common.state.channels.actions.UnarchiveChannel
5252
import io.getstream.chat.android.ui.common.state.channels.actions.UnblockUser
53-
import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteChannel
5453
import io.getstream.chat.android.ui.common.state.channels.actions.UnmuteUser
5554
import io.getstream.chat.android.ui.common.state.channels.actions.UnpinChannel
5655
import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo
@@ -96,8 +95,8 @@ public fun ChannelOptions(
9695
*
9796
* Actions vary by channel type:
9897
* - **DM:** View Info, Mute/Unmute User, Block/Unblock User, Archive Chat, Delete Chat
99-
* - **Group (owner):** View Info, Mute/Unmute Group, Archive Group, Delete Group
100-
* - **Group (member):** View Info, Mute/Unmute Group, Archive Group, Leave Group
98+
* - **Group (owner):** View Info, Archive Group, Delete Group
99+
* - **Group (member):** View Info, Archive Group, Leave Group
101100
*
102101
* @param selectedChannel The currently selected channel.
103102
* @param isMuted If the channel is muted or not.
@@ -136,7 +135,6 @@ public fun buildDefaultChannelActions(
136135
} else {
137136
buildGroupChannelActions(
138137
selectedChannel = selectedChannel,
139-
isMuted = isMuted,
140138
ownCapabilities = ownCapabilities,
141139
optionVisibility = optionVisibility,
142140
channelName = channelName,
@@ -171,7 +169,7 @@ private fun buildDmChannelActions(
171169
onViewInfoAction = onViewInfoAction,
172170
),
173171
buildDmMuteUserAction(
174-
isVisible = optionVisibility.isMuteChannelVisible,
172+
isVisible = optionVisibility.isMuteUserVisible,
175173
otherUserId = otherUserId,
176174
selectedChannel = selectedChannel,
177175
viewModel = viewModel,
@@ -292,14 +290,13 @@ private fun buildDmDeleteAction(
292290

293291
/**
294292
* Builds channel actions for group channels.
295-
* - **Owner (has DELETE_CHANNEL):** View Info, Mute/Unmute Group, Archive Group, Delete Group
296-
* - **Member (no DELETE_CHANNEL):** View Info, Mute/Unmute Group, Archive Group, Leave Group
293+
* - **Owner (has DELETE_CHANNEL):** View Info, Archive Group, Delete Group
294+
* - **Member (no DELETE_CHANNEL):** View Info, Archive Group, Leave Group
297295
*/
298296
@Suppress("LongMethod", "LongParameterList")
299297
@Composable
300298
private fun buildGroupChannelActions(
301299
selectedChannel: Channel,
302-
isMuted: Boolean,
303300
ownCapabilities: Set<String>,
304301
optionVisibility: ChannelOptionItemVisibility,
305302
channelName: String,
@@ -308,7 +305,6 @@ private fun buildGroupChannelActions(
308305
): List<ChannelAction> {
309306
val canLeaveChannel = ownCapabilities.contains(ChannelCapabilities.LEAVE_CHANNEL)
310307
val canDeleteChannel = ownCapabilities.contains(ChannelCapabilities.DELETE_CHANNEL)
311-
val canMuteChannel = ownCapabilities.contains(ChannelCapabilities.MUTE_CHANNEL)
312308

313309
return listOfNotNull(
314310
if (optionVisibility.isViewInfoVisible) {
@@ -320,12 +316,6 @@ private fun buildGroupChannelActions(
320316
} else {
321317
null
322318
},
323-
buildGroupMuteAction(
324-
canMuteChannel = optionVisibility.isMuteChannelVisible && canMuteChannel,
325-
isMuted = isMuted,
326-
selectedChannel = selectedChannel,
327-
viewModel = viewModel,
328-
),
329319
buildGroupPinAction(
330320
canPinChannel = optionVisibility.isPinChannelVisible,
331321
selectedChannel = selectedChannel,
@@ -479,33 +469,6 @@ private fun buildGroupArchiveAction(
479469
null -> null
480470
}
481471

482-
/**
483-
* Builds the mute action for group channels, using "Mute Group" / "Unmute Group" labels.
484-
*/
485-
@Composable
486-
private fun buildGroupMuteAction(
487-
canMuteChannel: Boolean,
488-
isMuted: Boolean,
489-
selectedChannel: Channel,
490-
viewModel: ChannelListViewModel,
491-
): ChannelAction? = if (canMuteChannel) {
492-
when (isMuted) {
493-
true -> UnmuteChannel(
494-
channel = selectedChannel,
495-
label = stringResource(id = R.string.stream_compose_selected_channel_menu_unmute_group),
496-
onAction = { viewModel.unmuteChannel(selectedChannel) },
497-
)
498-
499-
false -> MuteChannel(
500-
channel = selectedChannel,
501-
label = stringResource(id = R.string.stream_compose_selected_channel_menu_mute_group),
502-
onAction = { viewModel.muteChannel(selectedChannel) },
503-
)
504-
}
505-
} else {
506-
null
507-
}
508-
509472
/**
510473
* Preview of [ChannelOptions].
511474
*

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import io.getstream.chat.android.models.querysort.QuerySortByField
5252
import io.getstream.chat.android.models.querysort.QuerySorter
5353
import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction
5454
import io.getstream.chat.android.ui.common.utils.extensions.defaultChannelListFilter
55-
import io.getstream.chat.android.ui.common.utils.extensions.isOneToOne
5655
import io.getstream.log.taggedLogger
5756
import io.getstream.result.call.toUnitCall
5857
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -434,8 +433,7 @@ public class ChannelListViewModel(
434433
channelMutes,
435434
typingChannels,
436435
channelDraftMessages,
437-
globalMuted,
438-
) { state, channelMutes, typingChannels, channelDraftMessages, userMutes ->
436+
) { state, channelMutes, typingChannels, channelDraftMessages ->
439437
when (state) {
440438
ChannelsStateData.NoQueryActive,
441439
ChannelsStateData.Loading,
@@ -460,8 +458,6 @@ public class ChannelListViewModel(
460458
channelItems = createChannelItems(
461459
channels = state.channels,
462460
channelMutes = channelMutes,
463-
userMutes = userMutes,
464-
currentUser = user.value,
465461
typingEvents = typingChannels,
466462
draftMessages = channelDraftMessages.takeIf { isDraftMessageEnabled } ?: emptyMap(),
467463
),
@@ -805,41 +801,25 @@ public class ChannelListViewModel(
805801
*
806802
* @param channels The channels to show.
807803
* @param channelMutes The list of channels muted for the current user.
808-
* @param userMutes The list of users muted by the current user.
809-
* @param currentUser The currently logged in user.
804+
*
810805
*/
811-
@Suppress("LongParameterList")
812806
private fun createChannelItems(
813807
channels: List<Channel>,
814808
channelMutes: List<ChannelMute>,
815-
userMutes: List<Mute>,
816-
currentUser: User?,
817809
typingEvents: Map<String, TypingEvent>,
818810
draftMessages: Map<String, DraftMessage>,
819811
): List<ItemState.ChannelItemState> {
820812
val mutedChannelIds = channelMutes.map { channelMute -> channelMute.channel?.cid }.toSet()
821-
val mutedUserIds = userMutes.mapNotNullTo(mutableSetOf()) { it.target?.id }
822813
return channels.map {
823814
ItemState.ChannelItemState(
824815
channel = it,
825-
isMuted = it.cid in mutedChannelIds || it.isOneToOneMutedByUser(currentUser, mutedUserIds),
816+
isMuted = it.cid in mutedChannelIds,
826817
typingUsers = typingEvents[it.cid]?.users ?: emptyList(),
827818
draftMessage = draftMessages[it.cid],
828819
)
829820
}
830821
}
831822

832-
/**
833-
* Checks if a 1:1 channel is muted via user mute (i.e. the other member is muted).
834-
*/
835-
private fun Channel.isOneToOneMutedByUser(currentUser: User?, mutedUserIds: Set<String>) =
836-
if (mutedUserIds.isEmpty() || currentUser == null || !isOneToOne(currentUser)) {
837-
false
838-
} else {
839-
val otherUser = members.find { it.user.id != currentUser.id }?.user
840-
otherUser != null && otherUser.id in mutedUserIds
841-
}
842-
843823
internal companion object {
844824
/**
845825
* Default value of number of channels to return when querying channels.

0 commit comments

Comments
 (0)