Support non-destructive command mode and command availability UX#6378
Support non-destructive command mode and command availability UX#6378
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
c079cca to
eb8d70c
Compare
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
WalkthroughThreads composer Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant MessageComposer
participant CommandSuggestionList
participant MessageComposerController
participant MessageComposerViewModel
participant SnackbarHostState
User->>MessageComposer: Tap command (in Edit mode)
MessageComposer->>CommandSuggestionList: Render with messageAction=Edit
CommandSuggestionList->>CommandSuggestionList: Check isAvailableFor(Edit)
CommandSuggestionList->>User: Show dimmed command (alpha)
User->>CommandSuggestionList: Tap disabled command
CommandSuggestionList->>MessageComposerController: selectCommand(unavailableCmd)
MessageComposerController->>MessageComposerController: Detect unavailable for Edit
MessageComposerController->>MessageComposerViewModel: Emit CommandUnavailable(Edit)
MessageComposerViewModel->>SnackbarHostState: Show snackbar (variant=Error)
SnackbarHostState->>User: Display "Not available while editing"
sequenceDiagram
participant User
participant MessageComposer
participant AttachmentPicker
participant MessageComposerController
participant CommandStash
User->>MessageComposer: Open attachment picker (Reply mode)
MessageComposer->>AttachmentPicker: Open with messageAction=Reply
AttachmentPicker->>AttachmentPicker: sortedByAvailability(Reply)
AttachmentPicker->>User: Show available/disabled commands
User->>AttachmentPicker: Select command
AttachmentPicker->>MessageComposerController: selectCommand(cmd)
rect rgba(100, 150, 200, 0.5)
MessageComposerController->>CommandStash: Capture pre-command input/attachments/mentions
end
MessageComposerController->>MessageComposerController: Set activeCommand
MessageComposerController->>AttachmentPicker: closePicker()
User->>MessageComposer: Clear command
MessageComposerController->>CommandStash: Restore stashed state
CommandStash->>MessageComposerController: Provide restored input/attachments/mentions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt (1)
198-202:⚠️ Potential issue | 🟡 MinorKeep the command sheet open for unavailable command taps.
Line 199 dismisses the modal before routing can reject edit/reply-unavailable commands, so a disabled tap shows feedback but also closes the command picker. Gate the dismissal on availability while still firing
actions.onCommandSelected(command)for the integrator contract.Suggested adjustment
+import io.getstream.chat.android.compose.ui.messages.composer.internal.isAvailableFor + ... onCommandSelected = { command -> - showCommandsPickerDialog = false + if (command.isAvailableFor(messageAction)) { + showCommandsPickerDialog = false + } actions.onCommandSelected(command) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt` around lines 198 - 202, Call actions.onCommandSelected(command) first and only set showCommandsPickerDialog = false when the command was actually accepted; specifically, invoke actions.onCommandSelected(command) before touching showCommandsPickerDialog and gate dismissal on either the boolean return value from actions.onCommandSelected (if you change its signature to return true when accepted) or on a concrete availability flag on the command (e.g., command.isAvailable / command.available) — keep messageAction untouched but use onCommandSelected’s result or command availability to decide whether to set showCommandsPickerDialog = false.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt (1)
68-80:⚠️ Potential issue | 🟠 MajorDo not expose
messageActionas a public parameter inAttachmentPicker.The parameter on line 80 is exposed in the public API signature, adding new surface area that conflicts with the PR objective of no public API additions. Although the default value
nullmaintains backward compatibility at call sites, the signature change affects binary compatibility for compiled consumers. Move the action-aware implementation to an internal composable and preserve the public signature by removing this parameter:Suggested refactor
`@Suppress`("LongMethod") `@Composable` public fun AttachmentPicker( attachmentsPickerViewModel: AttachmentsPickerViewModel, modifier: Modifier = Modifier, messageMode: MessageMode = MessageMode.Normal, actions: AttachmentPickerActions = remember(attachmentsPickerViewModel) { AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel) }, - messageAction: MessageAction? = null, +) { + AttachmentPickerInternal( + attachmentsPickerViewModel = attachmentsPickerViewModel, + modifier = modifier, + messageMode = messageMode, + actions = actions, + messageAction = null, + ) +} + +@Suppress("LongMethod") +@Composable +internal fun AttachmentPickerInternal( + attachmentsPickerViewModel: AttachmentsPickerViewModel, + modifier: Modifier = Modifier, + messageMode: MessageMode = MessageMode.Normal, + actions: AttachmentPickerActions = AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel), + messageAction: MessageAction? = null, ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt` around lines 68 - 80, The public AttachmentPicker composable currently exposes the messageAction parameter (AttachmentPicker(..., messageAction: MessageAction? = null)), which expands the public API; remove this parameter from the public signature and restore the original binary-compatible public API by creating an internal implementation that accepts messageAction and having the public AttachmentPicker call that internal version with messageAction = null. Concretely, add an internal composable (e.g., AttachmentPickerInternal or AttachmentPickerWithAction) that takes the same parameters plus messageAction: MessageAction?, move the action-aware logic into it, and change the public AttachmentPicker to the simpler signature (no messageAction) that delegates to the internal composable passing messageAction = null; keep symbols like AttachmentsPickerViewModel, AttachmentPickerActions.pickerDefaults, MessageMode and the remember(...) usage unchanged.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt (1)
192-194:⚠️ Potential issue | 🟡 MinorMirror the command availability contract on the stateless overload.
This public overload also receives every command tap, including visually unavailable commands, but its KDoc does not mention that callers must filter and surface feedback themselves.
📝 Proposed KDoc update
- * `@param` onCommandSelected Handler when the user taps on a command suggestion item. + * `@param` onCommandSelected Handler for every tap on a command suggestion item, regardless of the + * command's availability in the current composer action. This stateless overload does not filter + * unavailable commands or show feedback, so callers are responsible for both branches.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt` around lines 192 - 194, Update the public stateless overload's KDoc for MessageComposer to mirror the command availability contract: state clearly that the onCommandSelected handler will receive every command tap (including commands that are visually marked unavailable) and that callers are responsible for filtering availability and providing appropriate user feedback; reference the onCommandSelected parameter in the KDoc so consumers of the stateless overload understand they must check command availability and handle UI feedback themselves.
🧹 Nitpick comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt (1)
101-102: Includecontextin the remembered actions keys.
onCommandSelectedcapturesLocalContext.current, but therememberkeys do not include it. Addcontextto avoid stale Toast context if the composition local changes.♻️ Proposed tweak
- val actions = remember(attachmentsPickerViewModel, composerViewModel, composerState.action) { + val actions = remember(attachmentsPickerViewModel, composerViewModel, composerState.action, context) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt` around lines 101 - 102, The remember call that builds actions should include the LocalContext.current (context) in its key list to avoid capturing a stale Context used by onCommandSelected (which shows a Toast); update the remember invocation that currently lists attachmentsPickerViewModel, composerViewModel, composerState.action to also include context so the lambda recreates when the composition local changes and the Toast uses the current Context.stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt (1)
61-87: Add Toast assertions to verify user-visible feedback for unavailable commands.The tests currently verify callback suppression but skip the Toast branch. Since
routeCommandSelection()explicitly callsToast.makeText()for unavailable commands in reply/edit modes, the tests should assert this side effect to catch resource mismatches or missing Toast calls.🧪 Example assertion to add
+import io.getstream.chat.android.compose.R +import org.robolectric.shadows.ShadowToast + `@Test` fun `Unavailable command in reply mode does not invoke onAvailable`() { var received: Command? = null routeCommandSelection( @@ assertNull(received) + assertEquals( + context.getString(R.string.stream_compose_message_composer_command_unavailable_in_reply), + ShadowToast.getTextOfLatestToast(), + ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt` around lines 61 - 87, Extend the two tests (`Unavailable command in reply mode does not invoke onAvailable` and `Unavailable command in edit mode does not invoke onAvailable`) to also assert that a Toast is shown when the command is unavailable: after calling routeCommandSelection(...) (which internally calls Toast.makeText), verify the Toast was created/shown (using the test harness you already use for UI feedback, e.g., Robolectric ShadowToast or your existing Toast verifier) and assert its text matches the expected user-facing message for unavailable commands; keep the existing onAvailable null assertions intact and reference routeCommandSelection and Toast.makeText in your checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 1011-1019: The stash logic stores selected attachments but only
clears picker-selected attachments (_selectedAttachments) while leaving
_recordingAttachment intact, so a completed recording can remain attached when
entering command mode; update the stash/reset sequence in
MessageComposerController (around commandStash creation and the similar blocks
at the other locations) to also clear _recordingAttachment (set it to null or
appropriate empty state) before calling syncAttachments(), and ensure
commandStash includes the recording attachment if intended (or explicitly
excludes it) so recordings are correctly removed from the composer when stashing
commands and restored on cancel.
---
Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt`:
- Around line 68-80: The public AttachmentPicker composable currently exposes
the messageAction parameter (AttachmentPicker(..., messageAction: MessageAction?
= null)), which expands the public API; remove this parameter from the public
signature and restore the original binary-compatible public API by creating an
internal implementation that accepts messageAction and having the public
AttachmentPicker call that internal version with messageAction = null.
Concretely, add an internal composable (e.g., AttachmentPickerInternal or
AttachmentPickerWithAction) that takes the same parameters plus messageAction:
MessageAction?, move the action-aware logic into it, and change the public
AttachmentPicker to the simpler signature (no messageAction) that delegates to
the internal composable passing messageAction = null; keep symbols like
AttachmentsPickerViewModel, AttachmentPickerActions.pickerDefaults, MessageMode
and the remember(...) usage unchanged.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt`:
- Around line 198-202: Call actions.onCommandSelected(command) first and only
set showCommandsPickerDialog = false when the command was actually accepted;
specifically, invoke actions.onCommandSelected(command) before touching
showCommandsPickerDialog and gate dismissal on either the boolean return value
from actions.onCommandSelected (if you change its signature to return true when
accepted) or on a concrete availability flag on the command (e.g.,
command.isAvailable / command.available) — keep messageAction untouched but use
onCommandSelected’s result or command availability to decide whether to set
showCommandsPickerDialog = false.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 192-194: Update the public stateless overload's KDoc for
MessageComposer to mirror the command availability contract: state clearly that
the onCommandSelected handler will receive every command tap (including commands
that are visually marked unavailable) and that callers are responsible for
filtering availability and providing appropriate user feedback; reference the
onCommandSelected parameter in the KDoc so consumers of the stateless overload
understand they must check command availability and handle UI feedback
themselves.
---
Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt`:
- Around line 101-102: The remember call that builds actions should include the
LocalContext.current (context) in its key list to avoid capturing a stale
Context used by onCommandSelected (which shows a Toast); update the remember
invocation that currently lists attachmentsPickerViewModel, composerViewModel,
composerState.action to also include context so the lambda recreates when the
composition local changes and the Toast uses the current Context.
In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt`:
- Around line 61-87: Extend the two tests (`Unavailable command in reply mode
does not invoke onAvailable` and `Unavailable command in edit mode does not
invoke onAvailable`) to also assert that a Toast is shown when the command is
unavailable: after calling routeCommandSelection(...) (which internally calls
Toast.makeText), verify the Toast was created/shown (using the test harness you
already use for UI feedback, e.g., Robolectric ShadowToast or your existing
Toast verifier) and assert its text matches the expected user-facing message for
unavailable commands; keep the existing onAvailable null assertions intact and
reference routeCommandSelection and Toast.makeText in your checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a6c9784f-4859-487a-be46-9ed321c4634a
⛔ Files ignored due to path filters (5)
stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.pngis excluded by!**/*.png
📒 Files selected for processing (20)
stream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandAvailability.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRouting.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.ktstream-chat-android-compose/src/main/res/values/strings.xmlstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandAvailabilityTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt
b4a9389 to
b4a47f7
Compare
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt (2)
62-62: Stale/incorrect test tag.
testTag("Stream_SuggestionListGiphyButton")on a genericCommandSuggestionItemis misleading — it's used for every command, not just Giphy. This predates the PR so feel free to defer, but while you're touching this composable it may be worth renaming to e.g.Stream_CommandSuggestionItem(sweep call sites/UI tests).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt` at line 62, The test tag on the CommandSuggestionItem composable is stale/misleading; replace the hardcoded .testTag("Stream_SuggestionListGiphyButton") in the CommandSuggestionItem implementation with a generic, descriptive tag such as "Stream_CommandSuggestionItem" (or similar) and update any test or call sites that assert on the old tag to use the new tag; search for the CommandSuggestionItem composable and its .testTag usage to change the string constant and adjust UI tests that reference "Stream_SuggestionListGiphyButton".
56-63: Minor: consider accessibility semantics for disabled commands.Lowering alpha to 0.5 visually communicates "disabled", but the
Rowremains fully clickable with no semantic signal. For TalkBack users the item is indistinguishable from an enabled one. Since the product decision is that taps must still fire (to surface the explanatory Toast), consider adding aModifier.semantics { /* stateDescription or disabled = true-ish hint */ }or a content description suffix so assistive tech communicates the unavailable state. Not a blocker.Also note: the
.alpha(...)is placed after.clickable, so the ripple is also dimmed — that's the typical desired effect here, just confirming it's intentional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt` around lines 56 - 63, The Row for CommandSuggestionItem visually dims disabled items via .alpha but lacks accessibility semantics; update the modifier chain used in Row (the modifier passed into Row, the .clickable { onCommandSelected(command) } block, and the enabled boolean) to include Modifier.semantics { stateDescription = if (!enabled) "Unavailable" else "Available" } or a similar disabled hint so assistive tech (TalkBack) communicates the unavailable state; keep the clickable behavior (taps still fire) and ensure the semantics modifier references the same enabled flag (and testTag "Stream_SuggestionListGiphyButton") so screen readers get the suffix/description without changing visual/ripple behavior.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt (1)
60-64: LGTM — consider documenting replay/buffer semantics.Exposing
eventsas a pass-throughSharedFlowis clean. Since this is a public API for one-shot events, consumers will care whether late collectors receive replays. Consider mentioning the underlying flow'sreplay/extraBufferCapacitybehavior (fromMessageComposerController.events) in the KDoc so integrators don't accidentally miss or double-handle events on configuration changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt` around lines 60 - 64, Add KDoc to the public val events on MessageComposerViewModel clarifying the replay/extraBufferCapacity semantics inherited from MessageComposerController.events: state that this SharedFlow is a one-shot events stream of MessageComposerViewEvent, whether late collectors will receive replays (replay = X) and how extraBufferCapacity affects backpressure or missed events on configuration changes, and note recommended consumer behavior (e.g., collect in lifecycleScope or use replay cache handling) so integrators know if they must handle duplicates on reconnection or configuration change; reference the underlying MessageComposerController.events in the KDoc.stream-chat-android-compose/src/main/res/values-it/strings.xml (1)
156-158: Minor: alphabetical ordering broken.The rest of this file is alphabetically sorted, but these three new
stream_compose_message_composer_command(s)_unavailable_*entries are inserted between..._file_not_supported(line 155) and..._instant_commands(line 159), which placescommand_unavailable_in_*out of order (should come beforefile_not_supported). Same issue applies to the other localized files andvalues/strings.xmlfor consistency. Not functional, just housekeeping.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/res/values-it/strings.xml` around lines 156 - 158, These three new string resources (stream_compose_message_composer_command_unavailable_in_edit, stream_compose_message_composer_command_unavailable_in_reply, stream_compose_message_composer_commands_unavailable_in_edit) are inserted out of alphabetical order; move them so their keys appear alphabetically before stream_compose_message_composer_file_not_supported in this values-it/strings.xml and apply the same reorder to the matching entries in other locale files and values/strings.xml to keep all string files consistently sorted.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt (1)
78-90: Apply the external modifier to the outermost composable.
modifieris currently attached to the innerSurface; move it to the wrappingBoxand keep theSurfacemodifier internal.♻️ Proposed adjustment
- * `@param` modifier Modifier applied to the inner [Surface] (e.g. to control width or padding). + * `@param` modifier Modifier applied to the snackbar container. @@ - Box(modifier = Modifier.padding(StreamTokens.spacingMd)) { + Box(modifier = modifier.padding(StreamTokens.spacingMd)) { Surface( - modifier = modifier.shadow(4.dp, shape = SnackbarShape), + modifier = Modifier.shadow(4.dp, shape = SnackbarShape),Based on learnings, “In Compose UI components within the stream-chat-android-compose module, always apply modifiers to the outermost composable in the hierarchy.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt` around lines 78 - 90, The external modifier passed to StreamSnackbar should be applied to the outermost composable: move the parameter `modifier` from the inner Surface to the wrapping Box (combine it with the existing Box padding, e.g. use modifier.then(Modifier.padding(StreamTokens.spacingMd)) or modifier.padding(...)) and keep the Surface's internal modifiers (shadow(4.dp, shape = SnackbarShape), shape, color, etc.) unchanged; update the StreamSnackbar function so Box receives the external modifier and Surface uses only component-internal modifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt`:
- Around line 66-106: The current edit branch in AttachmentCommandPicker returns
EmptyContent, which hides the command list and prevents onCommandSelected and
unavailable-command feedback from running; instead render the same command list
UI used in the non-edit branch but pass enabled = false for each CommandItem
when messageAction is an Edit so commands remain visible and tappable (use the
existing sortedCommands remember block and items iteration with CommandItem,
setting enabled to command.isAvailableFor(messageAction) && messageAction !is
Edit or simply false when Edit) to preserve feedback while showing disabled
state.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 431-436: The observer on _messageActions only re-sorts suggestions
but leaves a previously-selected activeCommand set even when the new action
(found as activeAction from actions.lastOrNull) no longer supports it; update
the handler so that before calling _state.update { it.copy(action =
activeAction) } you clear the active command if it's invalid for the new action
(e.g., set activeCommand/nullField to null or call the existing command-clear
method), then call handleCommandSuggestions(), and ensure buildNewMessage uses
the cleared state; reference _messageActions, activeAction, _state.update,
handleCommandSuggestions, activeCommand and buildNewMessage when making the
change.
---
Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt`:
- Line 62: The test tag on the CommandSuggestionItem composable is
stale/misleading; replace the hardcoded
.testTag("Stream_SuggestionListGiphyButton") in the CommandSuggestionItem
implementation with a generic, descriptive tag such as
"Stream_CommandSuggestionItem" (or similar) and update any test or call sites
that assert on the old tag to use the new tag; search for the
CommandSuggestionItem composable and its .testTag usage to change the string
constant and adjust UI tests that reference "Stream_SuggestionListGiphyButton".
- Around line 56-63: The Row for CommandSuggestionItem visually dims disabled
items via .alpha but lacks accessibility semantics; update the modifier chain
used in Row (the modifier passed into Row, the .clickable {
onCommandSelected(command) } block, and the enabled boolean) to include
Modifier.semantics { stateDescription = if (!enabled) "Unavailable" else
"Available" } or a similar disabled hint so assistive tech (TalkBack)
communicates the unavailable state; keep the clickable behavior (taps still
fire) and ensure the semantics modifier references the same enabled flag (and
testTag "Stream_SuggestionListGiphyButton") so screen readers get the
suffix/description without changing visual/ripple behavior.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`:
- Around line 78-90: The external modifier passed to StreamSnackbar should be
applied to the outermost composable: move the parameter `modifier` from the
inner Surface to the wrapping Box (combine it with the existing Box padding,
e.g. use modifier.then(Modifier.padding(StreamTokens.spacingMd)) or
modifier.padding(...)) and keep the Surface's internal modifiers (shadow(4.dp,
shape = SnackbarShape), shape, color, etc.) unchanged; update the StreamSnackbar
function so Box receives the external modifier and Surface uses only
component-internal modifiers.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt`:
- Around line 60-64: Add KDoc to the public val events on
MessageComposerViewModel clarifying the replay/extraBufferCapacity semantics
inherited from MessageComposerController.events: state that this SharedFlow is a
one-shot events stream of MessageComposerViewEvent, whether late collectors will
receive replays (replay = X) and how extraBufferCapacity affects backpressure or
missed events on configuration changes, and note recommended consumer behavior
(e.g., collect in lifecycleScope or use replay cache handling) so integrators
know if they must handle duplicates on reconnection or configuration change;
reference the underlying MessageComposerController.events in the KDoc.
In `@stream-chat-android-compose/src/main/res/values-it/strings.xml`:
- Around line 156-158: These three new string resources
(stream_compose_message_composer_command_unavailable_in_edit,
stream_compose_message_composer_command_unavailable_in_reply,
stream_compose_message_composer_commands_unavailable_in_edit) are inserted out
of alphabetical order; move them so their keys appear alphabetically before
stream_compose_message_composer_file_not_supported in this values-it/strings.xml
and apply the same reorder to the matching entries in other locale files and
values/strings.xml to keep all string files consistently sorted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 2a4d7608-9dfa-47d3-b8cc-23abb56b878e
⛔ Files ignored due to path filters (13)
stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_edit_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_reply_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_default.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_error.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_loading.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_message_only.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_success.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_with_action.pngis excluded by!**/*.png
📒 Files selected for processing (32)
stream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.ktstream-chat-android-compose/src/main/res/values-es/strings.xmlstream-chat-android-compose/src/main/res/values-fr/strings.xmlstream-chat-android-compose/src/main/res/values-hi/strings.xmlstream-chat-android-compose/src/main/res/values-in/strings.xmlstream-chat-android-compose/src/main/res/values-it/strings.xmlstream-chat-android-compose/src/main/res/values-ja/strings.xmlstream-chat-android-compose/src/main/res/values-ko/strings.xmlstream-chat-android-compose/src/main/res/values/strings.xmlstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPickerTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/StreamSnackbarTest.ktstream-chat-android-ui-common/api/stream-chat-android-ui-common.apistream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailability.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailabilityTest.kt
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt (1)
413-425:⚠️ Potential issue | 🟡 MinorUpdate LaunchedEffect dependency key to trigger when the error changes, not just the list count.
LaunchedEffect(validationErrors.size)fails to detect transitions wherefirstValidationErrorchanges but the list size remains the same (e.g., replacing oneAttachmentSizeExceedederror with another having differentmaxAttachmentSize). This prevents the snackbar/toast from displaying for the new error.Proposed effect key
- LaunchedEffect(validationErrors.size) { + LaunchedEffect(firstValidationError, errorMessage, snackbarHostState) { if (firstValidationError is ValidationError.ContainsLinksWhenNotAllowed || firstValidationError is ValidationError.AttachmentSizeExceeded🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt` around lines 413 - 425, LaunchedEffect is currently keyed on validationErrors.size which misses changes when firstValidationError or errorMessage change without size changing; update the effect key to include the actual error value(s) so it retriggers (e.g., use LaunchedEffect(firstValidationError, errorMessage) or LaunchedEffect(validationErrors) instead of LaunchedEffect(validationErrors.size)) and keep the existing snackbarHostState.showSnackbar call inside the effect so the snackbar appears for new/different ValidationError values.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt (1)
913-930:⚠️ Potential issue | 🟠 MajorEnsure .api file updates accompany public data class constructor changes.
Adding trailing parameters to public Kotlin
data classconstructors changes the JVM ABI. The project haskotlinx-binary-compatibility-validatorconfigured with.apifiles to track these changes. Confirm this PR includes updated.apifiles (run./gradlew apiDumpif needed) and that the team intentionally approves the binary-incompatible changes before merge.Also applies to: 1006-1009, 2353-2358, 2406-2415, 2484-2489, 2503-2510
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt` around lines 913 - 930, You added new constructor parameters to the public data class MessageComposerParams which changes the JVM ABI; run ./gradlew apiDump to regenerate the kotlinx-binary-compatibility-validator .api files, commit the updated .api entries, and confirm the team intentionally accepts this binary-incompatible change before merging; apply the same process for other affected public constructors mentioned (lines referenced: the other data classes around the file such as the ones at the ranges 1006-1009, 2353-2358, 2406-2415, 2484-2489, 2503-2510) so the API dump reflects all added trailing params.
🧹 Nitpick comments (4)
stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt (1)
65-72: Add a small delay to avoid tight UiAutomator polling.When the selector is absent or the text has not settled, this loop repeatedly calls
device.findObjectuntil timeout, which can burn CPU and make e2e runs noisier.Proposed refinement
while (System.currentTimeMillis() < endTime) { val actual = currentTextOrNull() if (actual != null) { lastText = actual val matches = if (mustBeEqual) actual == expectedText else actual.contains(expectedText) if (matches) return actual } + Thread.sleep(50) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt` around lines 65 - 72, The tight polling loop in Wait.kt repeatedly calls currentTextOrNull() until timeout; add a small sleep between iterations (e.g., 25–100ms) to prevent busy-waiting: after checking/matching the actual text in the while loop, when there is no match (or actual is null) call Thread.sleep with a short delay and properly handle InterruptedException by restoring the thread interrupt status; update the loop around currentTextOrNull() (the while block shown) to include this delay so UiAutomator polling is throttled.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt (1)
122-152: Apply the caller modifier only to the outer row.
modifieris already consumed by the outerRowat Line 123; reusing it on the nestedRowat Line 152 can duplicate caller-provided padding/background/test semantics.♻️ Proposed fix
Row( - modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingSm), ) {Based on learnings, in Compose UI components within
stream-chat-android-compose, always apply modifiers to the outermost composable in the hierarchy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt` around lines 122 - 152, The nested Row inside AttachmentCommandPicker is reusing the caller-supplied "modifier" (already applied to the outer Row), which can duplicate caller padding/background/test semantics; change the inner Row to use a fresh Modifier (e.g., Modifier or Modifier.fillMaxWidth()) instead of "modifier" so the caller modifier is only applied once at the outer Row; update the inner Row's "modifier = modifier" to "modifier = Modifier" (or another local Modifier chain) to fix.stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt (1)
78-90: Applymodifierto the outer snackbar container.The caller-provided modifier should wrap the whole snackbar, including the container padding; keep the shadow on the inner
Surface.Proposed modifier placement
- * `@param` modifier Modifier applied to the inner [Surface] (e.g. to control width or padding). + * `@param` modifier Modifier applied to the outer snackbar container. @@ - Box(modifier = Modifier.padding(StreamTokens.spacingMd)) { + Box(modifier = modifier.padding(StreamTokens.spacingMd)) { Surface( - modifier = modifier.shadow(4.dp, shape = SnackbarShape), + modifier = Modifier.shadow(4.dp, shape = SnackbarShape),Based on learnings, in Compose UI components within
stream-chat-android-compose, always apply modifiers to the outermost composable in the hierarchy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt` around lines 78 - 90, The passed-in modifier is currently applied to the inner Surface; update StreamSnackbar so the caller-provided modifier is applied to the outer Box (wrapping the padding) and the Surface keeps the shadow/shape styling (keep Surface.shadow(4.dp, shape = SnackbarShape) but remove modifier from Surface and instead apply modifier.then(Modifier.padding(StreamTokens.spacingMd)) to the Box), ensuring the outermost composable uses the external Modifier while the Surface retains its visual decorations.stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt (1)
1327-1355: Assert the send guard directly.This test verifies the snackbar event, but not the “message is not sent” part from the test name. Add an explicit
never()verification so a future implementation cannot emit the event and still callsendMessage.Proposed test assertion
// When / Then controller.events.test { - controller.sendMessage(message, callback = mock()) + val callback: Call.Callback<Message> = mock() + controller.sendMessage(message, callback = callback) advanceUntilIdle() assertEquals(MessageComposerViewEvent.CommandUnavailable(replyAction), awaitItem()) + verify(fixture.chatClient, never()).sendMessage(any(), any(), any(), any()) cancelAndIgnoreRemainingEvents() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt` around lines 1327 - 1355, The test currently asserts the CommandUnavailable event but not that the message was prevented from being sent; after calling controller.sendMessage(message, callback = mock()) and advanceUntilIdle(), add a Mockito verification that the underlying sending backend is never invoked (e.g., verify(mockedChatClient or mockedMessageSender, never()).sendMessage(...) or the concrete backend method used by MessageComposerController) to ensure sendMessage on the transport/client was not called. Place this verify(...) immediately after the event assertion to explicitly assert the "message is not sent" behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt`:
- Line 40: The assertion currently calls
Channel.messagePreview.waitForText(expectedPreview) and then compares to
trimEnd(), which can mask timeouts when only trailing whitespace differs; change
the wait to a trim-aware or substring wait so the UI is considered ready before
trimming—e.g., call the wait helper with a substring/trim option or wait for
expectedPreview.trimEnd() using a waitForSubstring/trim-aware overload of
Channel.messagePreview.waitForText, then assertEquals against the trimmed text.
In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Line 1752: The public API removed legacy JVM signatures (e.g.,
AttachmentPicker and AttachmentPickerParams constructors lacking MessageAction)
which is a binary-breaking change; restore compatibility by adding JVM-overload
methods/constructors that match the old descriptors (provide
AttachmentPicker(...) overloads and AttachmentPickerParams(...) constructors
without the MessageAction parameter) that delegate to the new implementations,
or alternatively mark this change as an intentional breaking API and regenerate
the API dump; ensure you update/verify symbols AttachmentPicker,
AttachmentPickerParams and any other affected top-level functions listed (1941,
2191–2201, 2264–2280, 2305–2317, 2344–2359, 4522–4531, 4711–4748) and then run
./gradlew apiDump to refresh the .api file.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt`:
- Line 65: The system command picker is being dismissed too early in
AttachmentSystemPicker when a command item is tapped; move or guard the
bottom-sheet dismissal so it only runs if the command is available (the same
availability check used by AttachmentPickerActions.consumePickerSession). Update
the tap handler (e.g., the onClick/onCommandSelected in AttachmentSystemPicker)
to first check the command availability and only call the picker dismissal/close
routine when available, otherwise skip dismissal and let
AttachmentPickerActions.consumePickerSession handle unavailable commands.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 160-171: The LaunchedEffect is currently keyed by Unit so swapping
MessageComposerViewModel keeps collecting the old viewModel.events; change the
effect to key on the ViewModel instance (e.g., LaunchedEffect(viewModel) or
LaunchedEffect(viewModel.events)) so collection cancels and restarts when the
MessageComposerViewModel changes, then continue collecting viewModel.events and
showing snackbars via snackbarHostState as before.
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 1248-1254: The suppressSuggestionsInEditMode function currently
emits MessageComposerViewEvent.CommandUnavailable for any trigger token; change
it to first check whether there are any registered commands and whether the
current CommandText (use CommandText parsing helpers) could match at least one
command before emitting. Specifically, inside suppressSuggestionsInEditMode
(which reads config.activeCommandEnabled, activeAction and
CommandText.isTrigger(messageText)), query the commands list (from the same
source used elsewhere for suggestions) and only call _state.update { ... } and
_events.tryEmit(MessageComposerViewEvent.CommandUnavailable(action)) when there
is at least one command in the channel but none match the parsed trigger; if no
commands exist or the prefix cannot possibly match any command, simply return
false without emitting.
---
Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 413-425: LaunchedEffect is currently keyed on
validationErrors.size which misses changes when firstValidationError or
errorMessage change without size changing; update the effect key to include the
actual error value(s) so it retriggers (e.g., use
LaunchedEffect(firstValidationError, errorMessage) or
LaunchedEffect(validationErrors) instead of
LaunchedEffect(validationErrors.size)) and keep the existing
snackbarHostState.showSnackbar call inside the effect so the snackbar appears
for new/different ValidationError values.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt`:
- Around line 913-930: You added new constructor parameters to the public data
class MessageComposerParams which changes the JVM ABI; run ./gradlew apiDump to
regenerate the kotlinx-binary-compatibility-validator .api files, commit the
updated .api entries, and confirm the team intentionally accepts this
binary-incompatible change before merging; apply the same process for other
affected public constructors mentioned (lines referenced: the other data classes
around the file such as the ones at the ranges 1006-1009, 2353-2358, 2406-2415,
2484-2489, 2503-2510) so the API dump reflects all added trailing params.
---
Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt`:
- Around line 122-152: The nested Row inside AttachmentCommandPicker is reusing
the caller-supplied "modifier" (already applied to the outer Row), which can
duplicate caller padding/background/test semantics; change the inner Row to use
a fresh Modifier (e.g., Modifier or Modifier.fillMaxWidth()) instead of
"modifier" so the caller modifier is only applied once at the outer Row; update
the inner Row's "modifier = modifier" to "modifier = Modifier" (or another local
Modifier chain) to fix.
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`:
- Around line 78-90: The passed-in modifier is currently applied to the inner
Surface; update StreamSnackbar so the caller-provided modifier is applied to the
outer Box (wrapping the padding) and the Surface keeps the shadow/shape styling
(keep Surface.shadow(4.dp, shape = SnackbarShape) but remove modifier from
Surface and instead apply
modifier.then(Modifier.padding(StreamTokens.spacingMd)) to the Box), ensuring
the outermost composable uses the external Modifier while the Surface retains
its visual decorations.
In
`@stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt`:
- Around line 65-72: The tight polling loop in Wait.kt repeatedly calls
currentTextOrNull() until timeout; add a small sleep between iterations (e.g.,
25–100ms) to prevent busy-waiting: after checking/matching the actual text in
the while loop, when there is no match (or actual is null) call Thread.sleep
with a short delay and properly handle InterruptedException by restoring the
thread interrupt status; update the loop around currentTextOrNull() (the while
block shown) to include this delay so UiAutomator polling is throttled.
In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt`:
- Around line 1327-1355: The test currently asserts the CommandUnavailable event
but not that the message was prevented from being sent; after calling
controller.sendMessage(message, callback = mock()) and advanceUntilIdle(), add a
Mockito verification that the underlying sending backend is never invoked (e.g.,
verify(mockedChatClient or mockedMessageSender, never()).sendMessage(...) or the
concrete backend method used by MessageComposerController) to ensure sendMessage
on the transport/client was not called. Place this verify(...) immediately after
the event assertion to explicitly assert the "message is not sent" behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8c2af675-bf0f-4dec-961d-7cc458af8222
⛔ Files ignored due to path filters (13)
stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_edit_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_reply_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_default.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_error.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_loading.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_message_only.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_success.pngis excluded by!**/*.pngstream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_with_action.pngis excluded by!**/*.png
📒 Files selected for processing (35)
stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.ktstream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.ktstream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.ktstream-chat-android-compose/src/main/res/values-es/strings.xmlstream-chat-android-compose/src/main/res/values-fr/strings.xmlstream-chat-android-compose/src/main/res/values-hi/strings.xmlstream-chat-android-compose/src/main/res/values-in/strings.xmlstream-chat-android-compose/src/main/res/values-it/strings.xmlstream-chat-android-compose/src/main/res/values-ja/strings.xmlstream-chat-android-compose/src/main/res/values-ko/strings.xmlstream-chat-android-compose/src/main/res/values/strings.xmlstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPickerTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.ktstream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/StreamSnackbarTest.ktstream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.ktstream-chat-android-ui-common/api/stream-chat-android-ui-common.apistream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailability.ktstream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.ktstream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailabilityTest.kt
…r and related components
…mands in the message composer. - Move `CommandAvailability` and related tests to `ui-common` module. - Add `MessageComposerNotice` interface and `CommandUnavailable` notice type to track transient composer feedback. - Update `MessageComposerState` and `MessageComposerController` to support emitting and dismissing notices. - Refactor `MessageComposerController` to block command selection and suggestions when in `Edit` mode or when moderation commands are used in `Reply` mode, emitting a notice instead. - Update UI components and internal routing to use the relocated `isAvailableFor` utility.
…ndle transient UI feedback as one-shot side effects. - Rename `MessageComposerNotice` to `MessageComposerViewEvent`. - Replace the `notices` list in `MessageComposerState` with an `events` `SharedFlow` in `MessageComposerController`. - Remove `dismissNotice()` and `emitNotice()` in favor of direct event emission. - Update `MessageComposerController` to emit `CommandUnavailable` events when commands are triggered in incompatible modes (e.g., during editing). - Update unit tests to collect and verify events using the `test` library.
- Removed `CommandSelectionRouting.kt` and its associated tests, moving command availability logic into the `MessageComposerViewModel`. - Added `snackbarHostState` to `MessageComposerParams` and `MessageComposer` to support displaying validation errors and transient notifications like unavailable commands. - Updated `MessageComposer` to collect events from `MessageComposerViewModel` and show relevant snackbar messages. - Updated `AttachmentPickerMenu` and `ChatComponentFactory` to align with the new command selection and snackbar logic. - Exposed `events` flow in `MessageComposerViewModel` and updated public API signatures.
…ve the suggestions menu
- Introduced `StreamSnackbarVariant` (Default, Error, Success, Loading) and `StreamSnackbarVisuals`. - Updated `StreamSnackbar` to render leading icons or a loading indicator based on the variant. - Added new previews and Paparazzi snapshot tests for all snackbar variants. - Updated `StreamSnackbarTest` to use landscape orientation for snapshots.
…it and reply modes
…poserController`
- Add `CancelCommandRequired` event to `MessageComposerViewEvent`.
- Implement logic in `MessageComposerController` to block actions (Edit/Reply) if an incompatible command is active.
- Introduce auto-selection of commands when typing `/{command} `.
- Add localized string resources for "Cancel command to edit/reply" across multiple languages.
- Update UI components to display snackbars for the new event.
- Add comprehensive unit tests for command auto-selection and action blocking.
Introduced a `Config.activeCommandEnabled` check to preserve pre-DS-030 behavior for the XML SDK. When disabled, the controller skips command availability validation in edit/reply modes and maintains server-provided command ordering instead of sorting by availability.
…r the current message action.
8a1a9a1 to
0b0025d
Compare
|


Goal
As per https://getstream.slack.com/archives/C0A3JU6C6G3/p1776782919458249
moderation_setin reply) are dimmed and sorted last; tapping one shows an Error snackbar and keeps the attachment picker open. Typing/in edit is blocked./name + spaceauto-enters command mode. Send-time refuses typed/pasted commands that conflict with the current action.Implementation
ui-common—MessageComposerControllergainsevents: SharedFlow<MessageComposerViewEvent>, a privateCommandStash, stash/restore onselectCommand/clearActiveCommand/clearData, and an action-change hook that re-derives suggestions. NewCommand.isAvailableFor(MessageAction?)+List<Command>.sortedByAvailability(…)(@InternalStreamChatApi). New non-sealed publicMessageComposerViewEventwithCommandUnavailableandCancelCommandRequiredvariants.performMessageActionemitsCancelCommandRequiredand returns early when the new action conflicts with the active command.sendMessageruns a text-level guard (InlineCommandPattern) for typed/pasted/moderation …in reply mode.handleCommandSuggestionsauto-selects on^/([a-z]+) $; the stash discriminator treats that form as a pure trigger so cancel doesn't re-auto-select.compose—MessageComposerViewModelforwardsevents; the VM-boundMessageComposercollects them into a hoistableSnackbarHostState(Error variant).SnackbarPopupnow anchors to the composerRow. Suggestion items gainenabled(0.5 alpha).AttachmentCommandPickerrenders empty state in edit;AttachmentPickerActionsstays open on unavailable taps.StreamSnackbargainsDefault/Error/Success/Loadingvariants; validation snackbars migrated to Error. Toast-basedCommandSelectionRoutingdeleted.Translations — 5 new keys × 7 locales:
command_unavailable_in_{edit,reply},commands_unavailable_in_edit,cancel_command_to_{edit,reply}.API — Purely additive (new public types, trailing-defaulted params).
apiDumprefreshed.Tests — Controller tests cover stash transitions, edit/reply guards, re-sort, event emission, action blocking, auto-select, chip cancel after auto-select, send-time guard.
CommandAvailabilityTestfor classification. Paparazzi snapshots refreshed.🎨 UI Changes
Screen_recording_20260423_162543.webm
Screen_recording_20260422_160351.webm
Screen_recording_20260422_160323.webm
Testing
Compose sample app, channel with
/giphy(fun_set) and/mute(moderation_set).activeCommandEnabled = true(Compose default).hello+ attach, pick/giphy, cancel → restored.draft, pick/giphy, typecat, cancel →draft./, pick/giphy(recording hidden), cancel → recording back.draft+ attach → pick/giphy→ type → send → composer empty./blocked. Edit a message, type/→ no popup, Error snackbar "Not available while editing"./→ moderation commands at bottom, dimmed. Tap/mute→ Error snackbar. Tap/giphy→ activates./(popup open), swipe reply → popup re-orders.+→ Commands → empty state.+→ Commands, tap/mute→ picker stays, snackbar fires./mute, swipe reply → reply not applied, snackbar "Cancel command to reply". Same for Edit./giphy, swipe reply → reply applies,/giphystays active./mute(trailing space) → chip appears, input clears. Tap×→ chip removed, input empty (not restored to/mute).activeCommandEnabled = false) type/giphy→ no chip./mute @someone, send → not sent, Error snackbar "Not available while replying".Summary by CodeRabbit
New Features
Tests