Skip to content

Unify MessageComposer state and expose read-only StateFlows on ViewModels#6279

Open
andremion wants to merge 4 commits intov7from
remove-derived-state-in-message-composer
Open

Unify MessageComposer state and expose read-only StateFlows on ViewModels#6279
andremion wants to merge 4 commits intov7from
remove-derived-state-in-message-composer

Conversation

@andremion
Copy link
Contributor

@andremion andremion commented Mar 20, 2026

Goal

Stop duplicating composer UI in extra flows; expose StateFlow only so state is read through MessageComposerState (and setMessageInput / actions) instead of mutable flows.

Implementation

  • MessageComposerController: one _state; removed parallel flows (cooldownTimer, suggestions, messageMode, lastActiveAction, public ownCapabilities, etc.); _messageInput / _messageActions with public StateFlow.
  • MessageComposerViewModel (Compose + XML): dropped delegating properties; messageInput is StateFlow. Call sites/tests/API dumps updated.
    Breaking: use messageComposerState for those fields; don’t assign messageInput.value — use setMessageInput.

🎨 UI Changes

No intentional visual change.

Testing

  • Type, send, cooldown (if slow mode).
  • / commands and mention @ suggestions.
  • Thread: enter, reply, leave thread; attachment picker height.
  • Reply/edit dismiss.
  • Run Compose + UI Components samples.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Consolidated message composer state management into a single unified state object for improved clarity and consistency.
    • Made message input property read-only to prevent unintended mutations.
    • Removed individual state accessor methods from the public API; state access now routes through the centralized composer state object.

@andremion andremion added the pr:breaking-change Breaking change label Mar 20, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@andremion andremion marked this pull request as ready for review March 20, 2026 14:07
@andremion andremion requested a review from a team as a code owner March 20, 2026 14:07
@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.68 MB 0.43 MB 🟡
stream-chat-android-ui-components 10.60 MB 10.98 MB 0.38 MB 🟡
stream-chat-android-compose 12.81 MB 12.08 MB -0.73 MB 🚀

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Walkthrough

This PR refactors the message composer architecture by consolidating multiple public mutable state flows (cooldown timer, validation errors, suggestions, message mode, capabilities, and actions) into a single read-only messageComposerState holder, making direct flow access internal, converting messageInput to read-only, and updating all dependent code to reference the consolidated state.

Changes

Cohort / File(s) Summary
Message Composer ViewModel API (Compose)
stream-chat-android-compose/api/stream-chat-android-compose.api
Removed 7 public getter methods for individual flows; changed getMessageInput() return type from MutableStateFlow to StateFlow.
Message Composer ViewModel Implementation (Compose)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt
Removed public properties: cooldownTimer, validationErrors, mentionSuggestions, commandSuggestions, linkPreviews, messageMode, lastActiveAction, ownCapabilities; changed messageInput from MutableStateFlow<MessageInput> to StateFlow<MessageInput>; removed related imports.
Message Composer ViewModel API (UI Components)
stream-chat-android-ui-components/api/stream-chat-android-ui-components.api
Removed 7 public getter methods; updated getMessageInput() return type from MutableStateFlow to StateFlow.
Message Composer ViewModel Implementation (UI Components)
stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt
Removed public properties: cooldownTimer, validationErrors, mentionSuggestions, commandSuggestions, messageMode, lastActiveAction, ownCapabilities; changed messageInput to StateFlow<MessageInput>.
Message Composer Controller
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
Converted state, messageInput, messageActions to private backing (_state, _messageInput, _messageActions) with read-only StateFlow exposure; removed public cooldownTimer, validationErrors, mentionSuggestions, commandSuggestions, linkPreviews, messageMode flows; refactored internal logic to read/write through consolidated state; updated setMessageMode implementation with new draft save/fetch orchestration.
Attachment Picker Menu
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt
Changed messageMode source from direct composerViewModel.messageMode collection to composerViewModel.messageComposerState.messageMode.
Chat Fragment (Sample App)
stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt
Consolidated two separate lifecycle-aware collectors (messageMode and lastActiveAction) into single collector of messageComposerState; derived both values from consolidated state.
Message Composer ViewModel Tests (Compose)
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt
Removed assertions on viewModel.messageMode, viewModel.ownCapabilities, viewModel.commandSuggestions.size, viewModel.mentionSuggestions.size; updated to access via messageComposerState.value; updated suggestion selections to use consolidated state source.
Message Composer ViewModel Tests (UI Components)
stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt
Removed direct assertions on messageMode, commandSuggestions, mentionSuggestions, ownCapabilities; updated to validate via messageComposerState and pass suggestions from consolidated state into selection methods.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Bouncing with joy through refactored code
Eight scattered flows now dance as one,
Where once there flowed a tangled road,
The state consolidated—elegantly done! ✨
No more public leaks, just clean design,
The composer now shines, oh so fine! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main goal: unifying MessageComposer state and converting public flows to read-only StateFlows on ViewModels.
Description check ✅ Passed The description includes Goal, Implementation, UI Changes, and Testing sections matching the template structure, but omits the contributor/reviewer checklists and GIF.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch remove-derived-state-in-message-composer
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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/api/stream-chat-android-compose.api`:
- Line 7082: Update the UNRELEASED CHANGELOG under the
stream-chat-android-compose → Changed section to document that getMessageInput()
now returns a read-only StateFlow (not MutableStateFlow), which prevents direct
assignment via messageInput.value; note the migration path instructing consumers
to call the provided setMessageInput(...) API instead of mutating
messageInput.value and mention this is an intentional API shape change for
immutability.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 927-928: The controller is exposing its internal MutableSet
(selectedMentions) into public state, causing later mutations (+= and clear())
to break snapshot immutability; change the _state.update call in
MessageComposerController to publish an immutable copy (e.g.,
selectedMentions.toSet() or HashSet(selectedMentions)) instead of the live
MutableSet, and when filterMentions() clears the backing set call _state.update
{ it.copy(selectedMentions = emptySet()) } (or publish an explicit empty
snapshot) so consumers never observe the mutable backing instance.
- Around line 533-540: The current setMessageMode publishes the new mode only
after saveDraftMessage(prev) which can suspend, causing races; change the order
so setMessageMode first updates/publishes the new mode via _state.update {
it.copy(messageMode = messageMode) } (while still capturing previousMode for the
draft), then call saveDraftMessage(previousMode) and finally
fetchDraftMessage(messageMode); update the implementation in setMessageMode (and
keep isSameMessageMode check) to ensure the new mode is visible immediately and
avoid stale reads from concurrent setMessageMode calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 89036ccf-92f2-472f-8a2f-017b0bfa8566

📥 Commits

Reviewing files that changed from the base of the PR and between 65d5819 and 7067046.

📒 Files selected for processing (9)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
  • stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt
  • stream-chat-android-ui-components/api/stream-chat-android-ui-components.api
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageComposerViewModel.kt
  • stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt

… the single source of truth for UI properties including `cooldownTimer`, `validationErrors`, `mentionSuggestions`, `commandSuggestions`, and `messageMode`.
…by exposing immutable `StateFlow` instead of `MutableStateFlow`.
…t ran, _state.value.messageMode was still the old mode, so overlapping setMessageMode calls could read stale mode.
…te. Further += / clear() changed that object in place, so you lost clear “snapshot” semantics and StateFlow might not emit when only the set contents changed.
@andremion andremion force-pushed the remove-derived-state-in-message-composer branch from 74dcd51 to aeb30b0 Compare March 20, 2026 14:52
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
70.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:breaking-change Breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant