Skip to content

Commit 8e8ea57

Browse files
committed
Sort command suggestions by availability based on the current message action.
1 parent ca24ee8 commit 8e8ea57

4 files changed

Lines changed: 60 additions & 4 deletions

File tree

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.items
2828
import androidx.compose.material3.Icon
2929
import androidx.compose.material3.Text
3030
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.remember
3132
import androidx.compose.ui.Alignment
3233
import androidx.compose.ui.Modifier
3334
import androidx.compose.ui.draw.alpha
@@ -51,6 +52,7 @@ import io.getstream.chat.android.ui.common.state.messages.Edit
5152
import io.getstream.chat.android.ui.common.state.messages.MessageAction
5253
import io.getstream.chat.android.ui.common.state.messages.Reply
5354
import io.getstream.chat.android.ui.common.state.messages.composer.isAvailableFor
55+
import io.getstream.chat.android.ui.common.state.messages.composer.sortedByAvailability
5456

5557
@Composable
5658
internal fun AttachmentCommandPicker(
@@ -76,9 +78,12 @@ internal fun AttachmentCommandPicker(
7678
style = ChatTheme.typography.headingSmall,
7779
color = ChatTheme.colors.textPrimary,
7880
)
81+
val sortedCommands = remember(commands, messageAction) {
82+
commands.sortedByAvailability(messageAction)
83+
}
7984
LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) {
8085
items(
81-
items = commands,
86+
items = sortedCommands,
8287
key = Command::name,
8388
) { command ->
8489
CommandItem(

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import io.getstream.chat.android.ui.common.state.messages.composer.MessageCompos
5050
import io.getstream.chat.android.ui.common.state.messages.composer.MessageValidator
5151
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState
5252
import io.getstream.chat.android.ui.common.state.messages.composer.isAvailableFor
53+
import io.getstream.chat.android.ui.common.state.messages.composer.sortedByAvailability
5354
import io.getstream.chat.android.ui.common.utils.AttachmentConstants
5455
import io.getstream.chat.android.ui.common.utils.extensions.addSchemeToUrlIfNeeded
5556
import io.getstream.chat.android.ui.common.utils.typing.TypingUpdatesBuffer
@@ -430,6 +431,9 @@ public class MessageComposerController(
430431
_messageActions.onEach { actions ->
431432
val activeAction = actions.lastOrNull { it is Edit || it is Reply }
432433
_state.update { it.copy(action = activeAction) }
434+
// Re-derive command suggestions so the list re-sorts by availability for the new
435+
// action and the edit-mode guard fires if a command is currently being typed.
436+
handleCommandSuggestions()
433437
}.launchIn(scope)
434438

435439
ownCapabilities.onEach { ownCapabilities ->
@@ -1065,9 +1069,15 @@ public class MessageComposerController(
10651069
* Toggles the visibility of the command suggestion list popup.
10661070
*/
10671071
public fun toggleCommandsVisibility() {
1068-
_state.update { s ->
1069-
val showCommands = s.commandSuggestions.isEmpty()
1070-
s.copy(commandSuggestions = if (showCommands) commands else emptyList())
1072+
_state.update {
1073+
val showCommands = it.commandSuggestions.isEmpty()
1074+
it.copy(
1075+
commandSuggestions = if (showCommands) {
1076+
commands.sortedByAvailability(activeAction)
1077+
} else {
1078+
emptyList()
1079+
},
1080+
)
10711081
}
10721082
}
10731083

@@ -1213,6 +1223,7 @@ public class MessageComposerController(
12131223
val suggestions = if (containsCommand) {
12141224
val commandPattern = messageText.removePrefix("/")
12151225
commands.filter { it.name.startsWith(commandPattern) }
1226+
.sortedByAvailability(action)
12161227
} else {
12171228
emptyList()
12181229
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,15 @@ public fun Command.isAvailableFor(action: MessageAction?): Boolean = when (actio
4141
else -> true
4242
}
4343

44+
/**
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.
47+
*
48+
* @param action The composer action currently active, or `null` when the composer is in its
49+
* default state.
50+
*/
51+
@InternalStreamChatApi
52+
public fun List<Command>.sortedByAvailability(action: MessageAction?): List<Command> =
53+
sortedByDescending { it.isAvailableFor(action) }
54+
4455
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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,35 @@ internal class MessageComposerControllerTest {
10481048
}
10491049
}
10501050

1051+
@Test
1052+
fun `Given slash typed When action becomes Reply Then suggestions re-sort by availability`() = runTest {
1053+
// Given: moderation command listed first, fun command second (default server order)
1054+
val muteCommand = Command("mute", "Mute user", "[@username]", "moderation_set")
1055+
val giphyCommand = Command("giphy", "Search GIFs", "[text]", "fun_set")
1056+
val repliedMessage = randomMessage(cid = CID)
1057+
val controller = Fixture()
1058+
.givenConfig(MessageComposerController.Config(activeCommandEnabled = true))
1059+
.givenAppSettings()
1060+
.givenAudioPlayer(mock())
1061+
.givenClientState(randomUser())
1062+
.givenGlobalState()
1063+
.givenChannelState(
1064+
configState = MutableStateFlow(Config(commands = listOf(muteCommand, giphyCommand))),
1065+
)
1066+
.get()
1067+
controller.setMessageInput("/")
1068+
advanceUntilIdle()
1069+
// Sanity: with no action, server order is preserved.
1070+
assertEquals(listOf(muteCommand, giphyCommand), controller.state.value.commandSuggestions)
1071+
1072+
// When
1073+
controller.performMessageAction(Reply(repliedMessage))
1074+
advanceUntilIdle()
1075+
1076+
// Then: available (giphy) first, unavailable (mute) last
1077+
assertEquals(listOf(giphyCommand, muteCommand), controller.state.value.commandSuggestions)
1078+
}
1079+
10511080
@Test
10521081
fun `Given reply mode When selectCommand called with fun_set command Then activeCommand is set`() = runTest {
10531082
// Given

0 commit comments

Comments
 (0)