Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1125,8 +1125,8 @@ public class MessageComposerController(
}

/**
* Sorts by availability when [Config.activeCommandEnabled] is `true`; returns the list
* unchanged in legacy mode.
* Applies [sortedByAvailability] when [Config.activeCommandEnabled] is `true`; returns the
* list unchanged in legacy mode.
*/
private fun List<Command>.orderedForComposer(): List<Command> =
if (config.activeCommandEnabled) sortedByAvailability(activeAction) else this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,20 @@ public fun Command.isAvailableFor(action: MessageAction?): Boolean = when (actio
}

/**
* Returns a new list with commands available for [action] first, followed by unavailable ones.
* The sort is stable, so the original order is preserved within each availability group.
* Returns a new list sorted by, in order:
* 1. Availability for [action] (available first).
* 2. Universal commands (`set != "moderation_set"`) before contextual ones.
*
* The sort is stable, so within each group the input order is preserved.
*
* @param action The composer action currently active, or `null` when the composer is in its
* default state.
*/
@InternalStreamChatApi
public fun List<Command>.sortedByAvailability(action: MessageAction?): List<Command> =
sortedByDescending { it.isAvailableFor(action) }
sortedWith(
compareByDescending<Command> { it.isAvailableFor(action) }
.thenByDescending { it.set != MODERATION_COMMAND_SET },
Comment thread
VelikovPetar marked this conversation as resolved.
)

private const val MODERATION_COMMAND_SET: String = "moderation_set"
Original file line number Diff line number Diff line change
Expand Up @@ -1081,10 +1081,15 @@ internal class MessageComposerControllerTest {
}

@Test
fun `Given slash typed When action becomes Reply Then suggestions re-sort by availability`() = runTest {
// Given
val muteCommand = randomCommand(set = MODERATION_SET)
val funCommand = randomCommand()
fun `Given slash typed Then suggestions are universal-first preserving backend order regardless of action`() = runTest {
// Given the backend hands out commands in an order where a moderation command precedes a
// universal one — the SDK's sort must move universal commands to the front while
// preserving the backend's relative order within each tier.
val banCommand = randomCommand(name = "ban", set = MODERATION_SET)
val giphyCommand = randomCommand(name = "giphy", set = "")
val muteCommand = randomCommand(name = "mute", set = MODERATION_SET)
val unbanCommand = randomCommand(name = "unban", set = MODERATION_SET)
val unmuteCommand = randomCommand(name = "unmute", set = MODERATION_SET)
val repliedMessage = randomMessage(cid = CID)
val controller = Fixture()
.givenConfig(MessageComposerController.Config(activeCommandEnabled = true))
Expand All @@ -1093,19 +1098,26 @@ internal class MessageComposerControllerTest {
.givenClientState(randomUser())
.givenGlobalState()
.givenChannelState(
configState = MutableStateFlow(Config(commands = listOf(muteCommand, funCommand))),
configState = MutableStateFlow(
Config(
commands = listOf(banCommand, giphyCommand, muteCommand, unbanCommand, unmuteCommand),
),
),
)
.get()
val expectedOrder = listOf(giphyCommand, banCommand, muteCommand, unbanCommand, unmuteCommand)

// When typing slash with the composer in its default state
controller.setMessageInput("/")
advanceUntilIdle()
assertEquals(listOf(muteCommand, funCommand), controller.state.value.commandSuggestions)

// When
// Then suggestions surface universal commands first, with backend order preserved within each tier.
assertEquals(expectedOrder, controller.state.value.commandSuggestions)

// And switching to Reply mode preserves the same order — no re-shuffle.
controller.performMessageAction(Reply(repliedMessage))
advanceUntilIdle()

// Then
assertEquals(listOf(funCommand, muteCommand), controller.state.value.commandSuggestions)
assertEquals(expectedOrder, controller.state.value.commandSuggestions)
}

@Test
Expand Down
Loading