Skip to content

Improve channel list TalkBack accessibility#6436

Merged
andremion merged 5 commits into
developfrom
fix/compose-channels-list-a11y
May 13, 2026
Merged

Improve channel list TalkBack accessibility#6436
andremion merged 5 commits into
developfrom
fix/compose-channels-list-a11y

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented May 13, 2026

Goal

Resolve TalkBack accessibility gaps on the Compose channel list:

  1. Each channel row announced as a generic "Channel item, …" with the unread badge speaking just a digit and the mute icon silent.
  2. Long-press / activation hints were generic ("double-tap to activate") with no verb.
  3. The screen header title ("Chats") was not reachable via TalkBack's swipe-by-heading gesture.
  4. Two latent TalkBack bugs: MessagePreviewContent announced previews twice, and a span computation in tagUser crashed the platform whenever an accessibility service tried to read a message whose text started with a mention.

Implementation

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

  1. Fix duplicated TalkBack readout in channel preview — switch MessagePreviewContent / DraftPreviewContent from semantics { text = … } to clearAndSetSemantics, so child Texts are no longer also merged into the surrounding clickable.
  2. Fix TalkBack crash on mentions at the start of a message — clamp the start - 1 backtrack in TextUtils.tagUser to 0. Without an accessibility service the bad span renders harmlessly; with TalkBack on, Spannable.setSpan(-1, …) throws and crashes the message list.
  3. Make channel list rows accessible to TalkBack — leaf-first a11y. Mute Icon carries a localized "muted" contentDescription; UnreadCountIndicator overrides its bare numeric Text with a plural label ("N unread messages") via clearAndSetSemantics on the badge (also benefits ThreadItem); outer Column drops the static "Channel item" placeholder and declares onClickLabel / onLongClickLabel on combinedClickable. Compose's natural merge composes name, timestamp, read-status, and preview in visual reading order. Adds four new strings + translations for all 7 supported locales; drops the obsolete stream_compose_cd_channel_item.
  4. Mark list-header title as a TalkBack heading — adds Modifier.semantics { heading() } to the title Text (and the offline "Disconnected" Text) in DefaultListHeaderCenterContent. Shared by ChannelListHeader and ThreadListHeader, so both screens gain heading navigation.

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:

  1. Header heading — set the TalkBack reading control to "Headings" (swipe up + right repeatedly). On the channels screen, swipe down — TalkBack should land on the title ("Chats") and announce it as a heading. Switch the reading control back to "Default" for the rest of the steps.
  2. Row announcement — focus a channel row. TalkBack should announce, in visual reading order: channel name, "muted" (if applicable), timestamp, "N unread messages" (if applicable), read-status (if applicable), message preview, "button", "double-tap to Open conversation, double-tap and hold to Open conversation options".
  3. Draft preview — focus a row whose last message is a saved draft. The "Draft: " portion must be announced once, not twice. Repeat for a non-draft row: sender prefix + message text must also be announced once.
  4. Mention crash — find or send a message whose text starts with a mention (e.g. "@Chewbacca hello"). With TalkBack on, opening the channel previously crashed with IndexOutOfBoundsException: setSpan (-1 …). It must now open normally.
  5. TalkBack navigation — with TalkBack on, focus a channel row and double-tap to confirm the conversation opens; long-press to confirm the channel options sheet appears.
  6. Localization — repeat step 2 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 throughout the channel list and messaging interface with improved action labels, semantic descriptions for unread counts, and heading navigation support.
  • Bug Fixes

    • Fixed potential crash when handling mention text at the beginning of messages.
  • Localization

    • Updated translations across Spanish, French, Hindi, Indonesian, Italian, Japanese, and Korean for channel item actions and unread message counts.

Review Change Stack

andremion added 4 commits May 12, 2026 15:14
`MessagePreviewContent` and `DraftPreviewContent` exposed a consolidated
`text` semantic on the parent `Row` to read the sender+message (or
"Draft:"+text) as one utterance, but the child `Text` composables still
merged their own text into the surrounding clickable's accessibility
tree, so TalkBack announced each preview twice. Switching to
`clearAndSetSemantics` replaces the children and emits the composed
preview once.
`tagUser` in `TextUtils` decrements the mention's start index by 1 to
include the leading `@` in the highlighted span. When the mentioned user
name appears at index 0 of the message text, that produces a span start
of -1. Compose tolerates the invalid span at render time, but TalkBack
asks the platform to convert the `AnnotatedString` to a spannable, where
`Spannable.setSpan` strictly validates and throws
`IndexOutOfBoundsException: setSpan (-1 ...) starts before 0`, crashing
the app the moment an accessibility service tries to read the message
list.

Clamp the highlight start to 0 in that edge case so the span stays
valid; visually the leading `@` only drops out of the highlight when no
`@` actually precedes the name.
Channel rows previously announced as "Channel item, …" with the unread
badge speaking just the digit, the mute icon silent, and no `onClickLabel`
on the row so TalkBack fell back to the generic "double-tap to activate"
hint with no verb.

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

- The mute `Icon` (both inline and trailing-bottom variants) now carries a
  localized "muted" `contentDescription`.
- `UnreadCountIndicator` overrides its inner numeric `Text` with a plural
  label ("N unread messages") via `clearAndSetSemantics` on the badge Box.
  Same component is used by `ThreadItem`, which benefits too.
- The outer `Column` drops the static "Channel item" placeholder and
  declares `onClickLabel = "Open conversation"` /
  `onLongClickLabel = "Open conversation options"` on `combinedClickable`.

Channel name, timestamp, read-status icon, and message preview already
expose their own semantics, so the natural merge picks them up unchanged.

Adds the four new strings across all 7 supported locales and drops the
now-unused `stream_compose_cd_channel_item` placeholder.
The channel-list and thread-list header titles already render in heading
typography but had no `heading()` semantics, so TalkBack's swipe-by-heading
gesture skipped them. Add `Modifier.semantics { heading() }` to both the
Connected title and the Offline "Disconnected" Text in
`DefaultListHeaderCenterContent`. The Connecting branch stays untouched
since it shows a transient loading indicator, not a heading.

Because `DefaultListHeaderCenterContent` is shared between
`ChannelListHeader` and `ThreadListHeader`, both screens gain heading
navigation from this single change.
@andremion andremion requested a review from a team as a code owner May 13, 2026 12:45
@andremion andremion added the pr:improvement Improvement label May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Walkthrough

This PR enhances accessibility across channel list UI by introducing Compose semantics improvements. ChannelItem adds click/long-click labels and muted status descriptions; UnreadCountIndicator implements pluralized accessibility descriptions; HeaderScaffold marks headers as semantic headings; MessagePreviewContent switches to clearAndSetSemantics; and TextUtils fixes a mention span rendering crash. Supporting string resources are added across all language locales.

Changes

Channel item and header accessibility semantics

Layer / File(s) Summary
Channel item accessibility semantics
src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt, src/main/res/values/strings.xml, src/main/res/values-*/strings.xml
ChannelItem updates combinedClickable to provide onClickLabel and onLongClickLabel, applies localized muted descriptions to muted icons, and removes obsolete imports. Base and localized string resources introduce stream_compose_channel_item_muted, stream_compose_channel_item_open, and stream_compose_channel_item_options.
Unread count with plural semantics
src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt, src/main/res/values/strings.xml, src/main/res/values-*/strings.xml
UnreadCountIndicator computes pluralized descriptions using pluralStringResource and applies them via clearAndSetSemantics. Plural resources stream_compose_channel_item_unread with one/other forms are added across all language locales.
Header semantic heading markers
src/main/java/io/getstream/chat/android/compose/ui/components/HeaderScaffold.kt
Header title and disconnected title text are marked with semantics { heading() } to enable screen readers to identify them as semantic headings.
Message preview semantics clarity
src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessagePreviewContent.kt
Two Row instances replace semantics { text = ... } with clearAndSetSemantics { text = ... } to establish message preview text as the dominant semantics.
Text mention span rendering safety
src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt
Mention styling start index is clamped to prevent negative values when mentions occur at text start, fixing crash during spannable conversion.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A rabbit hops through accessibility groves,
Adding labels where the channel list loves—
Headings marked with semantics so clear,
Plurals for unread, text bounds sincere. 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly describes the main objective of the changeset: improving TalkBack accessibility for the channel list in the Compose UI.
Description check ✅ Passed The description comprehensively covers Goal, Implementation (four detailed commits), and Testing sections. However, it lacks UI Changes screenshots/videos and the contributor/reviewer checklists.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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-channels-list-a11y

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

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.

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/res/values-es/strings.xml`:
- Around line 59-61: Update the Spanish string resource named
stream_compose_channel_item_options so the TalkBack action includes the verb;
change its value from "Opciones de conversación" to "Abrir opciones de
conversación" to match the action phrasing used in
stream_compose_channel_item_open and provide consistent, clear action labels.
🪄 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: f1a4f68c-bae7-4834-94fd-9744f2eb56c1

📥 Commits

Reviewing files that changed from the base of the PR and between 70033c8 and fbdf823.

📒 Files selected for processing (13)
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/HeaderScaffold.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessagePreviewContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.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

Comment thread stream-chat-android-compose/src/main/res/values-es/strings.xml Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 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.40 MB 0.00 MB 🟢

The English `stream_compose_channel_item_options` is "Open conversation
options" — verb + noun. All seven locale translations dropped the verb
("Opciones de conversación", "Options de la conversation", and so on),
which left the TalkBack action label inconsistent with the parallel
`stream_compose_channel_item_open` ("Abrir conversación" / "Ouvrir la
conversation" / …) where every locale already includes the verb.

Re-add the verb so each `_options` label mirrors the verb-prefix
convention its sibling `_open` label uses.
@sonarqubecloud
Copy link
Copy Markdown

@andremion andremion enabled auto-merge (squash) May 13, 2026 14:07
@andremion andremion merged commit dc95ce0 into develop May 13, 2026
16 checks passed
@andremion andremion deleted the fix/compose-channels-list-a11y branch May 13, 2026 14:21
@stream-public-bot stream-public-bot added the released Included in a release label May 22, 2026
@stream-public-bot
Copy link
Copy Markdown
Contributor

🚀 Available in v7.2.0

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

Labels

pr:improvement Improvement released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants