Skip to content

Commit a8e4a30

Browse files
authored
Sort instant commands universal-first preserving backend order (#6418)
Extends sortedByAvailability with a tier tie-breaker so command suggestions surface universal commands (set != moderation_set) before contextual ones. The sort is stable, so within each tier the input order from the backend's Channel.config.commands is preserved — no client-side opinion on relative order between commands of the same tier. Affects the Compose composer suggestion list (via MessageComposerController.orderedForComposer) and the Compose attachment command picker (AttachmentCommandPicker), which both call into sortedByAvailability.
1 parent 2c2e9e8 commit a8e4a30

3 files changed

Lines changed: 33 additions & 15 deletions

File tree

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,8 +1125,8 @@ public class MessageComposerController(
11251125
}
11261126

11271127
/**
1128-
* Sorts by availability when [Config.activeCommandEnabled] is `true`; returns the list
1129-
* unchanged in legacy mode.
1128+
* Applies [sortedByAvailability] when [Config.activeCommandEnabled] is `true`; returns the
1129+
* list unchanged in legacy mode.
11301130
*/
11311131
private fun List<Command>.orderedForComposer(): List<Command> =
11321132
if (config.activeCommandEnabled) sortedByAvailability(activeAction) else this

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailability.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@ public fun Command.isAvailableFor(action: MessageAction?): Boolean = when (actio
4242
}
4343

4444
/**
45-
* Returns a new list with commands available for [action] first, followed by unavailable ones.
46-
* The sort is stable, so the original order is preserved within each availability group.
45+
* Returns a new list sorted by, in order:
46+
* 1. Availability for [action] (available first).
47+
* 2. Universal commands (`set != "moderation_set"`) before contextual ones.
48+
*
49+
* The sort is stable, so within each group the input order is preserved.
4750
*
4851
* @param action The composer action currently active, or `null` when the composer is in its
4952
* default state.
5053
*/
5154
@InternalStreamChatApi
5255
public fun List<Command>.sortedByAvailability(action: MessageAction?): List<Command> =
53-
sortedByDescending { it.isAvailableFor(action) }
56+
sortedWith(
57+
compareByDescending<Command> { it.isAvailableFor(action) }
58+
.thenByDescending { it.set != MODERATION_COMMAND_SET },
59+
)
5460

5561
private const val MODERATION_COMMAND_SET: String = "moderation_set"

stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,10 +1081,15 @@ internal class MessageComposerControllerTest {
10811081
}
10821082

10831083
@Test
1084-
fun `Given slash typed When action becomes Reply Then suggestions re-sort by availability`() = runTest {
1085-
// Given
1086-
val muteCommand = randomCommand(set = MODERATION_SET)
1087-
val funCommand = randomCommand()
1084+
fun `Given slash typed Then suggestions are universal-first preserving backend order regardless of action`() = runTest {
1085+
// Given the backend hands out commands in an order where a moderation command precedes a
1086+
// universal one — the SDK's sort must move universal commands to the front while
1087+
// preserving the backend's relative order within each tier.
1088+
val banCommand = randomCommand(name = "ban", set = MODERATION_SET)
1089+
val giphyCommand = randomCommand(name = "giphy", set = "")
1090+
val muteCommand = randomCommand(name = "mute", set = MODERATION_SET)
1091+
val unbanCommand = randomCommand(name = "unban", set = MODERATION_SET)
1092+
val unmuteCommand = randomCommand(name = "unmute", set = MODERATION_SET)
10881093
val repliedMessage = randomMessage(cid = CID)
10891094
val controller = Fixture()
10901095
.givenConfig(MessageComposerController.Config(activeCommandEnabled = true))
@@ -1093,19 +1098,26 @@ internal class MessageComposerControllerTest {
10931098
.givenClientState(randomUser())
10941099
.givenGlobalState()
10951100
.givenChannelState(
1096-
configState = MutableStateFlow(Config(commands = listOf(muteCommand, funCommand))),
1101+
configState = MutableStateFlow(
1102+
Config(
1103+
commands = listOf(banCommand, giphyCommand, muteCommand, unbanCommand, unmuteCommand),
1104+
),
1105+
),
10971106
)
10981107
.get()
1108+
val expectedOrder = listOf(giphyCommand, banCommand, muteCommand, unbanCommand, unmuteCommand)
1109+
1110+
// When typing slash with the composer in its default state
10991111
controller.setMessageInput("/")
11001112
advanceUntilIdle()
1101-
assertEquals(listOf(muteCommand, funCommand), controller.state.value.commandSuggestions)
11021113

1103-
// When
1114+
// Then suggestions surface universal commands first, with backend order preserved within each tier.
1115+
assertEquals(expectedOrder, controller.state.value.commandSuggestions)
1116+
1117+
// And switching to Reply mode preserves the same order — no re-shuffle.
11041118
controller.performMessageAction(Reply(repliedMessage))
11051119
advanceUntilIdle()
1106-
1107-
// Then
1108-
assertEquals(listOf(funCommand, muteCommand), controller.state.value.commandSuggestions)
1120+
assertEquals(expectedOrder, controller.state.value.commandSuggestions)
11091121
}
11101122

11111123
@Test

0 commit comments

Comments
 (0)