Skip to content

Improve message list TalkBack accessibility#6440

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

Improve message list TalkBack accessibility#6440
andremion wants to merge 5 commits into
developfrom
fix/compose-messages-a11y

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented May 15, 2026

Goal

Resolve TalkBack accessibility gaps on the Compose message list (channel screen, threads screen). Issues a screen-reader user hits today:

  1. Every message row announces as a generic "Message item, …", the sender name is silently hidden (a clearAndSetSemantics wrapper kept only the testTag), and tap/long-press hints have no verb.
  2. Date separators ("Today", "Yesterday") and the unread divider are plain text — TalkBack's swipe-by-heading gesture skips them. The unread divider also renders "1 unread messages" for n = 1 (no <plurals>).
  3. The typing indicator is visual-only (avatars + animated dots). TalkBack picks nothing up — no focus, no announcement when someone starts typing.
  4. Reactions: the long-press reaction picker shows a checked background for the user's own reactions but exposes no role or selected state, so a TalkBack user can't tell which reactions they already chose. In-bubble reaction chips have no action verb.
  5. Image / video / file / link attachments announce "double-tap to activate" — no concrete verb for the action.

Implementation

Five self-contained commits, each scoped to one concern:

  1. Make message rows accessible to TalkBack — drop the static "Message item" placeholder on the outer Row; combinedClickable gains onClickLabel = "Open thread" (when canOpenThread) and onLongClickLabel = "Show message options" (when canOpenActions). MessageFooter sender-name Text drops its destructive clearAndSetSemantics { testTag = … } for Modifier.testTag(…) so the user name is spoken again. Drops the orphaned stream_compose_cd_message_item placeholder across all 8 locales.
  2. Make message list separators accessible as headings — add Modifier.semantics { heading() } to both the date and unread separators in DefaultMessageDateSeparatorContent / DefaultMessageUnreadSeparatorContent. Convert stream_compose_message_list_unread_separator from <string> to <plurals> across all 8 locales so n=1 reads as a singular.
  3. Announce typing indicator to TalkBack — wrap the indicator row in a polite live region with a localized description ("X is typing" / "X and Y are typing" / "N people are typing", reusing the existing stream_compose_channel_list_typing_* strings). The description is built by a small private helper to keep the composable under the method-length cap.
  4. Announce reaction state to TalkBackReactionToggle (the picker tile) swaps Modifier.clickable { onChange(!checked) } for Modifier.toggleable(value = checked, role = Role.Switch, onValueChange = onChange) so TalkBack announces the emoji plus "switch, on / off, double-tap to toggle". In-bubble reactions get onClickLabel = "Show reactions" on both ClusteredMessageReactions and per-chip ReactionChip clickables.
  5. Label message attachment tap action for TalkBack — add onClickLabel = "Open attachment" to the combinedClickable on MediaAttachmentContent, FileAttachmentContent, and LinkAttachmentContent.

All four new strings (stream_compose_message_item_open_thread, stream_compose_message_item_options, stream_compose_message_reactions_show, stream_compose_message_attachment_open) ship with translations for the 7 supported locales (es, fr, hi, in, it, ja, ko). No public API surface changes (apiDump clean).

Testing

Manual steps on the Compose sample with TalkBack ON and Settings → Accessibility → TalkBack → Verbosity → Speak usage hints enabled. Open any group channel with a mix of messages.

  1. Row announcement — focus an incoming text message in a group channel. TalkBack should announce ", , , , button, double-tap to Open thread (or activate), double-tap and hold to Show message options". Confirm the sender name is read for both regular and threaded messages. The previous "Message item, …" prefix should be gone.
  2. Read receipt — focus your own outgoing message. TalkBack should announce the read-receipt status ("Read" / "Delivered" / "Sent") instead of just timestamp.
  3. Deleted message — focus a deleted message. TalkBack should announce ", message deleted, …, disabled" — no spurious thread/options hints.
  4. Date separator — focus a date separator ("Today", "Yesterday"). TalkBack should append "heading" to the announcement. With reading control set to "Headings", swipe down/up should jump between date separators (skipping messages).
  5. Unread separator — scroll to a position with exactly one unread message below it. The separator should announce "1 unread message, heading" (singular). Repeat with multiple unread to confirm the plural form.
  6. Typing indicator — have someone start typing from another device (or another sample-app session). With TalkBack on, the bubble should announce " is typing" automatically when it appears, without you needing to focus it. Repeat with 2 and 3+ typing users.
  7. Reactions — in bubble — focus a message that has reactions. The reactions row reads "N reactions". Focus an individual reaction pill: TalkBack should say ", double-tap to Show reactions".
  8. Reactions — long-press picker — long-press a message, focus a reaction tile in the picker. TalkBack should announce ", switch, off, double-tap to toggle". Toggle the reaction on and refocus: the state should now be "on".
  9. Attachments — focus an image, video, file, and link preview in turn. Each should announce its existing type/filename followed by "double-tap to Open attachment" (previously "double-tap to activate").
  10. Localization — repeat steps 1, 7, 8, and 9 with the device language set to one of: Spanish, French, Hindi, Indonesian, Italian, Japanese, or Korean. The new labels should be translated.

Summary by CodeRabbit

  • New Features

    • Enhanced accessibility with descriptive action labels for opening attachments, message reactions, and thread interactions.
    • Improved pluralization support for unread message counts across multiple languages.
  • Localization

    • Updated string resources and translations in Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean.
  • Improvements

    • Refined message list item semantics and typing indicator descriptions for better screen reader support.
    • Simplified message author name test tag implementation.

Review Change Stack

andremion added 5 commits May 15, 2026 11:32
Message rows previously announced as "Message item, …" with the sender
name silently hidden from TalkBack and no action labels on the row's
clickable, so users heard the generic "double-tap to activate" hint with
no verb and could not tell who sent an incoming message in a group or
thread.

Push the a11y content down to the leaves so Compose's natural merge
composes them:

- `MessageFooter` sender name dropped its `clearAndSetSemantics { testTag
  = … }` wrapper, which had been wiping the Text's natural `text`
  semantic. Replaced with `Modifier.testTag(…)` so the testTag survives
  and the user name is announced.
- `MessageContainer` dropped the static "Message item" placeholder
  `contentDescription` on the outer Row. `combinedClickable` gains
  `onClickLabel = "Open thread"` (when `canOpenThread`) and
  `onLongClickLabel = "Show message options"` (when `canOpenActions`),
  each gated to the actions that actually fire.

Adds the two new strings across all 7 supported locales and drops the
now-unused `stream_compose_cd_message_item` placeholder from all 8
locales.
The date and unread separators in the message list are visual landmarks
that screen-reader users need to skim a long conversation, but neither
exposed a `heading()` semantic, so TalkBack's swipe-by-heading gesture
skipped them. The unread separator also rendered "1 unread messages"
because its label was a `<string>` with no plural form.

Add `Modifier.semantics { heading() }` to both the date separator
(`DefaultMessageDateSeparatorContent`) and the unread separator
(`DefaultMessageUnreadSeparatorContent`).

Convert `stream_compose_message_list_unread_separator` from `<string>`
to `<plurals>` across all 8 locales so n=1 reads as a singular ("1
unread message") and n>1 keeps the plural. Update the caller to use
`pluralStringResource`.
The typing indicator in the message list shows avatars and animated dots
but no text, so TalkBack picked nothing up — it neither focused the
bubble nor announced when someone started typing.

Wrap the indicator's outer Row in a polite live region with a localized
description ("X is typing" / "X and Y are typing" / "N people are
typing", reusing the existing `stream_compose_channel_list_typing_*`
strings since the wording is identical), and merge descendants so the
bubble announces as a single unit. The description-building logic is
extracted to a small private helper to keep the composable under
detekt's method-length cap.
The reaction toggles in the long-press reactions picker rendered a
checked background to indicate the user's own reactions, but exposed
nothing to TalkBack — no role, no selected state, just "double-tap to
activate". A user could not tell which reactions they had already
chosen. In-bubble reactions had a similar gap: each chip's clickable
had no `onClickLabel`, so TalkBack announced the generic "double-tap to
activate" with no verb.

- `ReactionToggle` swaps `Modifier.clickable { onChange(!checked) }` for
  `Modifier.toggleable(value = checked, role = Role.Switch,
  onValueChange = onChange)`. TalkBack now announces the emoji plus
  "switch, on / off, double-tap to toggle".
- `MessageReactions` adds `onClickLabel = "Show reactions"` to both the
  `ClusteredMessageReactions` row clickable and the per-chip
  `ReactionChip` clickable, so focusing a chip says "<emoji>, double
  tap to Show reactions".

Adds `stream_compose_message_reactions_show` across the 7 supported
locales. No public API surface change.
Image, video, file and link attachments in the message list previously
announced as "double-tap to activate" — generic, no verb. The
`combinedClickable` on each of those attachment containers had no
`onClickLabel`, so TalkBack fell back to the platform default.

Add `onClickLabel = "Open attachment"` to `MediaAttachmentContent`,
`FileAttachmentContent`, and `LinkAttachmentContent`. TalkBack now reads
"<type> attachment, double tap to Open attachment" for images and
videos, and "<filename>, <size>, double tap to Open attachment" for
files (the inner Texts already carry the name and size).

Adds `stream_compose_message_attachment_open` across the 7 supported
locales. No public API surface change.

Poll, location, giphy and audio attachments are left for a follow-up —
each has its own composable and a different interaction model. Multi-
attachment disambiguation (e.g. "Image 1 of 4") is also a separate
follow-up; it needs index/total plumbing through the multi-attachment
grid.
@andremion andremion added the pr:improvement Improvement label May 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 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.

@github-actions
Copy link
Copy Markdown
Contributor

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.40 MB 0.00 MB 🟢

@sonarqubecloud
Copy link
Copy Markdown

@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 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 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 10d97e9c-d09d-4c95-9b77-dd7a85f310ed

📥 Commits

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

📒 Files selected for processing (16)
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageFooter.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageReactions.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/reactions/ReactionToggle.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.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

Walkthrough

The PR systematically enhances accessibility across message UI components by introducing localized click labels for attachments and reactions, replacing manual semantic descriptions with modular label parameters, supporting pluralized unread message counts across locales, and refactoring the reaction toggle to use proper Compose toggle semantics with role support.

Changes

Message Accessibility and Interaction Enhancements

Layer / File(s) Summary
Localized String Resources Foundation
src/main/res/values/strings.xml, src/main/res/values-{es,fr,hi,in,it,ja,ko}/strings.xml
New message action strings (stream_compose_message_item_open_thread, stream_compose_message_item_options, stream_compose_message_attachment_open, stream_compose_message_reactions_show) added across all locales. stream_compose_message_list_unread_separator converted to pluralized <plurals> resource with one/other forms. Deprecated stream_compose_cd_message_item removed.
Attachment Content Click Labels
src/main/java/.../attachments/content/{FileAttachmentContent,LinkAttachmentContent,MediaAttachmentContent}.kt
File, Link, and Media attachment components now import stringResource and apply localized openAttachmentLabel to the combinedClickable modifier via onClickLabel, ensuring consistent "open attachment" accessibility across all attachment types.
Reaction Click Labels and Toggle Semantics
src/main/java/.../components/messages/MessageReactions.kt, src/main/java/.../components/reactions/ReactionToggle.kt
MessageReactions adds localized openLabel from string resources to both clustered reaction bubbles and individual ReactionChips. ReactionToggle refactors from clickable { onChange(!checked) } to toggleable(value = checked, role = Role.Switch, onValueChange = onChange) for proper toggle component semantics.
Message Container and Item Interaction Labels
src/main/java/.../messages/list/{MessageContainer,MessageItem}.kt
MessageContainer replaces manual contentDescription semantics with conditional onClickLabel/onLongClickLabel labels for thread and options actions. MessageItem adds heading semantics to date/unread separators, pluralized unread counts via string resources, and enhances typing indicator with contentDescription and live region (polite) support.
Message Footer Test Tag Cleanup
src/main/java/.../components/messages/MessageFooter.kt
Author-name test tagging simplified by replacing clearAndSetSemantics { testTag = ... } block with direct Modifier.testTag() call and removing associated semantics imports.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • gpunto
  • VelikovPetar

Poem

🐰 Labels bloom across the UI,
Each interaction speaks its why,
Toggles dance with semantics bright,
Plurals handle counts just right,
Accessibility takes flight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 'Improve message list TalkBack accessibility' directly reflects the main change: enhancing accessibility features for TalkBack screen readers in the message list UI.
Description check ✅ Passed The PR description is comprehensive, with clear Goal, Implementation (five commits), and detailed Testing sections covering all major changes and how to validate them.
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-messages-a11y

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@andremion andremion marked this pull request as ready for review May 15, 2026 12:11
@andremion andremion requested a review from a team as a code owner May 15, 2026 12:11
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