Skip to content

Improve message composer TalkBack accessibility: action verbs, banner merge, audio recording lifecycle#6441

Open
andremion wants to merge 5 commits into
developfrom
fix/compose-composer-a11y
Open

Improve message composer TalkBack accessibility: action verbs, banner merge, audio recording lifecycle#6441
andremion wants to merge 5 commits into
developfrom
fix/compose-composer-a11y

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented May 18, 2026

Goal

The Compose message composer was underserved on TalkBack: action buttons announced as generic "double-tap to activate" with no verb, the Edit / Reply banner read fragment-by-fragment, the audio recording flow had no lifecycle announcements, and TalkBack focus drifted out of the composer on state transitions. This PR brings the composer up to a usable TalkBack baseline.

Implementation

Three feature-scoped commits.

Action verbs on Send / Save / attach buttons.
FilledIconButton doesn't expose an onClickLabel, so TalkBack defaulted to "double-tap to activate" with no verb. Layered Modifier.semantics { onClick(label = …) { …; true } } on each button so the verb reads as Send message, Save changes, Open attachments / Close attachments (the attachments toggle pairs the label with the existing stateDescription). Extracted AttachmentPickerButton from MessageComposerLeadingContent to keep the parent under detekt's method-length cap.

Edit / Reply banner merge for a11y.
Wrapped the edit-indicator row and reply quoted-message in Modifier.semantics(mergeDescendants = true) {} so TalkBack reads each banner as a single chunk. Cancel icons announce the action they undo (Cancel editing / Cancel reply) instead of a generic "Cancel". When replying to your own message, the username Text leaf overrides contentDescription to "Reply to you" so the post-preposition pronoun form is grammatically correct in es / it / fr (the visible "You" label stays compact).

Audio recording lifecycle a11y.

  • Mic button: contentDescription includes the press-and-hold gesture hint, exposes Role.Button — TalkBack reads "Record audio message. Press and hold to record. Button.".
  • One-shot announcements via View.announceForAccessibility on recording state transitions: Idle → Hold announces "Recording started. Slide left to cancel, up to lock."; non-Idle → Idle announces "Recording cancelled" when gated by a cancelRequested flag set by wrapped onCancelRecording / onDeleteRecording (confirm / send paths stay silent and let the attachment item announce itself).
  • Focus moves on transitions into the recording UI: Hold → Locked lands on the merged recording-bar row, Locked → Overview lands on the merged playback row. Overview's outer Row is split into a separately-focusable play button + an inner focusable summary so the play action stays reachable.
  • Confirm button label is state-aware: "Send recording" when audioRecordingSendOnComplete is enabled, "Confirm recording" otherwise.
  • A 100 ms delay before each requestFocus lets Compose finish layout and the accessibility tree settle before we move focus, so it wins over TalkBack's fallback recovery.

All new strings translated across the 7 supported locales (es / fr / hi / in / it / ja / ko). No public API changes.

Testing

Enable TalkBack on a physical device or emulator. For each scenario, expected announcement is in bold.

1. Composer trailing-button verbs

  1. Open a channel, focus the trailing button while the input is empty → "Record audio message. Press and hold to record. Button."
  2. Type any character, focus the trailing button → "Send message" verb (instead of "Double-tap to activate").
  3. Trigger an Edit action on one of your messages, focus the trailing button → "Save changes" verb.

2. Composer leading-button label

  1. Focus the leading attachments button → "Open attachments. Collapsed."
  2. Tap to open the picker, focus again → "Close attachments. Expanded."

3. Edit / Reply banner

  1. Long-press your own message → Edit. Focus the banner → reads as one chunk including the original text.
  2. Focus the cancel icon → "Cancel editing, button".
  3. Long-press someone else's message → Reply. Focus the banner → reads "Reply to {name}, …" as one chunk.
  4. Repeat reply on your own message → "Reply to you, …" (not just "You").
  5. Focus the cancel icon → "Cancel reply, button".

4. Audio recording lifecycle

  1. Focus the mic button → "Record audio message. Press and hold to record. Button."
  2. Double-tap and hold to start recording. As the Hold state activates → "Recording started. Slide left to cancel, up to lock."
  3. Slide up to lock → focus moves to the recording-bar row, reads "Audio recording in progress".
  4. Tap the Stop button → focus moves to the playback row, reads "Audio recording". Play / Pause stays separately focusable.
  5. While in Locked or Overview, tap the Delete button → "Recording cancelled" announces.
  6. While in Hold, slide left to cancel → "Recording cancelled" announces and focus stays on the mic button.
  7. Toggle the audioRecordingSendOnComplete flag (ChatTheme.config.composer). In Overview, focus the Confirm button → reads "Send recording" when the flag is true, "Confirm recording" when false.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced accessibility for audio recording with automatic state announcements
    • Improved accessibility labels and descriptions for message composer controls
    • Better accessibility support for quoted message interactions
  • Localization

    • Updated user-facing strings across Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean languages with new labels for audio recording and message composition actions

Review Change Stack

@andremion andremion added the pr:improvement Improvement label May 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 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 (or ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@andremion
Copy link
Copy Markdown
Contributor Author

andremion commented May 18, 2026

@CodeRabbit review

@andremion andremion changed the title Improve composer TalkBack accessibility Improve message composer TalkBack accessibility: action verbs, banner merge, audio recording lifecycle May 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.82 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.02 MB 11.02 MB 0.00 MB 🟢
stream-chat-android-compose 12.40 MB 12.42 MB 0.02 MB 🟢

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Walkthrough

This PR comprehensively enhances accessibility across the Stream Chat Android Compose UI by introducing dedicated audio recording state announcements and focus management, adding semantic metadata to message composer components, and providing localized strings in seven language variants.

Changes

Accessibility Enhancements for Audio Recording and Message Composer

Layer / File(s) Summary
Audio Recording Accessibility Utilities
src/main/java/.../AudioRecordingAccessibility.kt
micButtonSemantics() modifier provides TalkBack button identification with localized content description; AnnounceRecordingTransitions() composable emits one-shot accessibility announcements for recording start (Idle→Hold) and conditional cancellation (return to Idle with cancelRequested flag).
Audio Recording Integration with Cancel Tracking
src/main/java/.../AudioRecordingButton.kt
AudioRecordingButton creates a wrapped cancelTrackingActions that sets cancelRequested on cancel/delete, wires this flag to AnnounceRecordingTransitions, passes the wrapped actions to content components, and replaces inline semantics setup with centralized .micButtonSemantics() modifier.
Audio Recording Content with Focus Management
src/main/java/.../AudioRecordingContent.kt
Locked and overview recording UI rows now use FocusRequester with delayed LaunchedEffect for programmatic focus control; confirmation icon's contentDescription computed conditionally based on audioRecordingSendOnComplete config; play/pause button extracted into dedicated private composable.
Quoted Message Accessibility
src/main/java/.../QuotedMessage.kt
Quoted message wrapper applies semantics(mergeDescendants = true) modifier; cancel icon includes localized contentDescription; username rendering refactored to conditionally set semantic contentDescription for the "you" + "reply banner" case.
Message Composer Edit Indicator Accessibility
src/main/java/.../MessageComposerEditIndicator.kt
Edit indicator card row gains semantics(mergeDescendants = true) modifier for improved accessibility node aggregation; cancel edit icon gets localized contentDescription.
Message Composer Send and Save Button Semantics
src/main/java/.../MessageComposerInputTrailingContent.kt
Send and save buttons now define localized semantics click labels and semantic click handlers; both invoke their respective onClick callbacks with accessibility framework support.
Attachment Picker Button Accessibility Refactor
src/main/java/.../MessageComposerLeadingContent.kt
Attachments button refactored into dedicated private AttachmentPickerButton composable with dynamic stateDescription and localized onClick semantics handler; preserves all icon rotation and styling.
English String Resources for Accessibility
src/main/res/values/strings.xml
New entries for audio recording (label, locked state, confirmation, state announcements), message composer actions (cancel edit/reply, save/send labels), attachment controls (open/close), and quoted-message "reply to you" string.
Localization Across Seven Languages
src/main/res/values-{es,fr,hi,in,it,ja,ko}/strings.xml
Corresponding string translations for Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean with equivalent audio recording, message composer action, attachment control, and quoted-message accessibility content.
Generated Composable API Updates
api/stream-chat-android-compose.api
One prior lambda accessor method in ComposableSingletons$AudioRecordingContentKt removed; three new lambda accessor methods added (all Function2 return type) reflecting structural changes in AudioRecordingContent.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • GetStream/stream-chat-android#6404: Composer attachment picker accessibility improvements, including content/stateDescription and semantic enhancements to MessageComposerLeadingContent.
  • GetStream/stream-chat-android#6374: Translation-only PR that introduces the same compose string resources (audio recording labels, states, quoted message) that this PR's code changes depend on.

Suggested Reviewers

  • gpunto
  • VelikovPetar

Poem

🐰 With focus and care, our audio now speaks,
Announcing when recording—let TalkBack hear the peak!
Buttons and actions, each labeled with grace,
Seven tongues sing the accessibility race.
From Idle to Hold, the transitions take flight,
Making Compose so accessible, crystal clear, bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main improvements: action verbs for buttons, banner merge for accessibility, and audio recording lifecycle enhancements.
Description check ✅ Passed The description includes Goal, Implementation, and Testing sections covering all required areas with specific TalkBack announcements and testing scenarios.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/compose-composer-a11y

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.

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingAccessibility.kt`:
- Around line 80-96: The LaunchedEffect is calling onTransitionConsumed()
unconditionally which can clear cancelRequested before the Idle ->
Idle-with-cancel transition is handled; change this so onTransitionConsumed() is
only invoked when a relevant transition was actually processed (i.e., when you
call view.announceForAccessibility for RecordingState.Hold or for
RecordingState.Idle with cancelRequested). Locate the LaunchedEffect block using
recordingState, previousWasIdle, cancelRequested and move the
onTransitionConsumed() call into the branches that perform
view.announceForAccessibility (or set a local handled flag and call
onTransitionConsumed() after the when only if handled is true).
🪄 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: 59b53ea3-4496-4593-9f9b-d2cb091ed485

📥 Commits

Reviewing files that changed from the base of the PR and between 677ebe7 and fd286d5.

📒 Files selected for processing (16)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/QuotedMessage.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingAccessibility.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingButton.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/AudioRecordingContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerEditIndicator.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputTrailingContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.kt
  • stream-chat-android-compose/src/main/res/values-es/strings.xml
  • stream-chat-android-compose/src/main/res/values-fr/strings.xml
  • stream-chat-android-compose/src/main/res/values-hi/strings.xml
  • stream-chat-android-compose/src/main/res/values-in/strings.xml
  • stream-chat-android-compose/src/main/res/values-it/strings.xml
  • stream-chat-android-compose/src/main/res/values-ja/strings.xml
  • stream-chat-android-compose/src/main/res/values-ko/strings.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
  • stream-chat-android-compose/api/stream-chat-android-compose.api

andremion added 4 commits May 18, 2026 15:20
`MessageComposerSendButton`, `MessageComposerSaveButton`, and the
attachments toggle in `MessageComposerLeadingContent` are wrapped in
Material3 `FilledIconButton`, which does not expose an `onClickLabel`.
TalkBack therefore announced each as `"<icon label>, button, double-tap
to activate"` — the generic activate hint had no verb.

Layer a `Modifier.semantics { onClick(label = …) { …; true } }` on each
button so TalkBack reads a concrete action:

- Send → `"Send message"` (`stream_compose_message_composer_send_action`).
- Save → `"Save changes"` (`stream_compose_message_composer_save_action`).
- Attachments → state-aware label that pairs with the existing
  `stateDescription`: `"Open attachments"` when collapsed,
  `"Close attachments"` when expanded
  (`stream_compose_message_composer_attachments_open` /
  `…_close`).

The semantics action call-through (`onClick(); true`) keeps the actual
click handler intact while overriding only the announced verb. The
attachments toggle button is also extracted into a private
`AttachmentPickerButton` composable to keep
`MessageComposerLeadingContent` under detekt's method-length cap.

Translates the four new strings across all 7 supported locales. No
public API surface change.
Wraps the edit-indicator row and the reply quoted-message in
`Modifier.semantics(mergeDescendants = true) {}` so TalkBack reads the
banner as a single chunk instead of fragment by fragment. Cancel icons
now announce the action being cancelled ("Cancel editing" / "Cancel
reply") rather than the generic "Cancel".

When replying to your own message, the username Text leaf overrides
`contentDescription` to "Reply to you" so TalkBack mirrors the
"Reply to {name}" symmetry while the visible "You" label stays compact.
Mic button content description includes the press-and-hold gesture
hint and exposes Role.Button, so TalkBack reads "Record audio message.
Press and hold to record. Button." without a misleading "double-tap
to activate" hint.

One-shot announcements via View.announceForAccessibility on recording
state transitions:
- Idle → Hold: "Recording started. Slide left to cancel, up to lock."
- non-Idle → Idle: "Recording cancelled", gated on a cancelRequested
  flag set by wrapped onCancelRecording / onDeleteRecording. Confirm
  and send paths leave the flag unset so they stay silent and let the
  attachment item announce itself.

Focus moves on state transitions:
- Hold → Locked: focus on the merged recording-bar row.
- Locked → Overview: focus on the merged playback row. Overview's
  outer Row is split into a separately-focusable play button + an
  inner focusable summary so the play action stays reachable.
- A 100ms delay before requestFocus gives Compose layout and the
  accessibility tree time to settle, beating TalkBack's fallback
  recovery from the previously-focused node.

Cancel and Delete focus is left to Compose's natural recovery — focus
returns to the mic button when it becomes visible again.
Move `onTransitionConsumed()` inside the announcement branches so the
caller's `cancelRequested` flag only clears when an announcement has
actually fired. The bug described in the review doesn't manifest with
the current state writers (the wrapped `onCancelRecording` /
`onDeleteRecording` set the flag and `setState(Idle)` in the same
Compose snapshot) but separating "transition observed" from "transition
consumed" tightens the helper's contract and makes it robust to future
changes to the flag writers or the `LaunchedEffect` keys.
@andremion andremion force-pushed the fix/compose-composer-a11y branch from fd286d5 to da0224f Compare May 18, 2026 14:21
@andremion andremion marked this pull request as ready for review May 18, 2026 14:22
@andremion andremion requested a review from a team as a code owner May 18, 2026 14:22
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
77.7% 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:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant