Skip to content

feat(ui): channel-list & message-preview accessibility pass#2827

Open
xsahil03x wants to merge 13 commits into
masterfrom
fix/a11y-fixes-batch-2
Open

feat(ui): channel-list & message-preview accessibility pass#2827
xsahil03x wants to merge 13 commits into
masterfrom
fix/a11y-fixes-batch-2

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Second batch of accessibility fixes across the channel list and thread list. Screen readers now speak one coherent row summary per channel / thread instead of navigating fragment-by-fragment, and preview text emits natural-language a11y labels instead of the visual "You: <message>" style.

  • AccessibilityTranslations gains the strings and formatters used by channel-list rows, thread-list rows, and message previews (delivery status, preview prefixes, muted / pinned / group / unread labels, formatRecentDateTime). All 11 locales ship native-language implementations.
  • New AccessibleMessagePreviewFormatter interface layered on top of MessagePreviewFormatter; consumers who only implements MessagePreviewFormatter still work via a visual-derived fallback.
  • Optional semanticsLabel on StreamChannelAvatar and StreamTimestamp.
  • StreamChannelListTile merges into a single accessible row summary; muted / pinned icons and unread badge are labeled; the sending indicator wraps in ExcludeSemantics inside the row to avoid scan noise.
  • StreamThreadListTile merges into a single accessible row summary with labeled unread badge.
  • voiceRecordingText fixed to sentence case.

Linear tickets

  • Fixes FLU-589 — attachment toggle announces open label while already expanded.
  • Fixes FLU-590 — channel list item announces avatar initial and repeats content without context.
  • Fixes FLU-591 — threads list item announces content as flat fragments without context.

Dependency

Depends on GetStream/stream-core-flutter#132 for the base StreamAvatar / StreamAvatarGroup / StreamAvatarStack semanticsLabel field. The pubspec pins to the head of that PR (c711bba); swap to a merged main ref once it lands, then flip to ready-for-review.

Test plan

  • melos run test:all — new preview / list-item / avatar / timestamp / thread-tile / localization tests pass.
  • melos run analyze — clean.
  • stream_chat_localizations package sits at 100% line coverage.
  • Manual pass with TalkBack and VoiceOver on a fixture app: channel list rows, thread list rows, message previews (own / group / DM / draft / poll / system), avatar labels, timestamp phrasing.

Notes on doc / CHANGELOG style

Public dartdocs and CHANGELOG entries are trimmed to match the sibling stream_core_flutter shape: one short summary line, blank separator, 1–3 sentence body. Docs describe the contract, not the implementation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Expanded screen-reader accessibility for message/draft previews using formatter-driven semantics labels.
    • Added optional semanticsLabel support for avatars, avatar stacks/groups, and timestamps.
    • Improved accessibility semantics for sending/delivery/read icons, channel list items, and thread list rows (including merged summaries).
    • Enhanced accessibility translations, including relative “recent” date/time and richer label coverage with pluralized unread/attachment counts.
  • Bug Fixes
    • Refined accessibility announcements to remove redundant/unwanted fragments and improve unread/pinned/muted phrasing.
    • Updated wording to “Voice recording” and “Toggle attachment picker” across supported accessibility strings.

xsahil03x and others added 7 commits July 15, 2026 14:00
…ttachment picker"

The tooltip was hard-coded to "Open attachment picker", so screen readers
announced "Open attachment picker" even when the picker was already
expanded. State-specific detail comes from the existing hint/onTapHint
(which already switch on isPickerOpen); the tooltip only needs to be
state-agnostic.

Fixes FLU-589.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…up, StreamUserAvatarStack

Thin pass-through of the new stream_core_flutter `semanticsLabel`
parameter. null (default) composes through; non-null exposes the
avatar as a single labeled image node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends `AccessibilityTranslations` with strings and formatters used by
the channel-list row and message-preview a11y wiring:

  * Preview prefixes: `outgoingMessagePreviewLabel` (`"You"`),
    `incomingMessagePreviewLabel({senderName})`, `pollPreviewLabel`,
    `draftPreviewLabel`, `systemMessagePreviewLabel`.
  * Delivery status: `messageSending` / `messageSent` /
    `messageDelivered` / `messageRead` status labels.
  * Channel-list row: `unreadMessagesLabel(count)`, `channelGroupLabel`,
    `channelMutedLabel`, `channelPinnedLabel`.
  * `formatRecentDateTime(DateTime)` — locale-aware bucketed timestamp
    for the last-message-date announcement.

Every supported locale (ca / de / en / es / fr / hi / it / ja / ko / no /
pt) ships a full native-language implementation.

Also fixes `voiceRecordingText` casing to sentence case
(`"Voice recording"`) — the English translation was the odd one out;
every other locale already used its native equivalent of sentence case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional interface `AccessibleMessagePreviewFormatter` extending
`MessagePreviewFormatter` with `formatMessageSemanticsLabel` and
`formatDraftMessageSemanticsLabel` — plain-text natural-language
previews suitable for `Text.semanticsLabel` / `Semantics.label`.
`StreamMessagePreviewText` and `StreamDraftMessagePreviewText` wire the
returned label into `Text.rich`'s `semanticsLabel`.

Existing custom formatters that only `implements MessagePreviewFormatter`
continue to work; the SDK falls back to a visual-derived label when the
formatter is not accessibility-aware.

`StreamMessagePreviewFormatter` (the default) implements the new
interface, composing:

  * A speaker prefix — `"You"` for own messages, sender's full name for
    other users in group channels, no prefix in 1-on-1 channels.
  * A type-context fragment — `"Poll"`, `"Photo"`, `"2 photos"`,
    `"Video"`, `"File"`, `"Voice recording"`, `"Link"`, `"Location"` —
    for message kinds whose visual icon carries meaning.
  * The message body (or attachment fallback).

Deleted and system messages skip the speaker prefix (state / event
descriptions, not authored content). Inline icon `WidgetSpan`
placeholders are stripped so screen readers never announce "object".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`StreamChannelAvatar` emits a default `"Group"` label for group
channels, restoring the "this is a group" context the multi-user avatar
stack conveys visually; direct-message avatars stay silent because the
counterpart's identity is already announced by the surrounding row
title. Callers can override via the new param; an empty string leaves
the avatar silent.

`StreamTimestamp` defaults to a bucketed natural-language phrasing via
`AccessibilityTranslations.formatRecentDateTime`, so screen readers get
a clear time reference even when the visible text is abbreviated
(`"2h"`, `"Sat"`, `"1/15"`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mary

Previously, screen readers walked the channel-list row fragment by
fragment — announcing avatar initials, timestamp, message body, and
unlabeled muted / pinned icons as separate stops. This commit wraps the
tile in `MergeSemantics`, labels the icons via
`channelMutedLabel` / `channelPinnedLabel`, and announces the unread
badge via `unreadMessagesLabel(count)`. Group avatars now emit the
default `"Group"` label; direct-message avatars stay silent.

`StreamSendingIndicator` labels its icon with the bare delivery-state
word (`"Sending"` / `"Sent"` / `"Delivered"` / `"Read"`) via
`AccessibilityTranslations`. The channel-list row wraps the indicator
in `ExcludeSemantics` so scan noise doesn't repeat "Sent" / "Read" on
every own-message row; the visual icon is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tyle

Drops rationale prose from the `semanticsLabel` field docs on
`StreamUserAvatar`, `StreamUserAvatarGroup`, and `StreamUserAvatarStack`
so they match the tight two-branch shape used across the sibling
`stream_core_flutter` avatar widgets. Same for the CHANGELOG entry —
trimmed to a single one-liner.

Also unconditionally wraps the placeholder initials in
`ExcludeSemantics` — the initials are decorative visual identifiers,
not information a screen reader can act on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a84303d-6356-46f9-97f4-cee7f16bcc63

📥 Commits

Reviewing files that changed from the base of the PR and between 4f30db9 and c8ee497.

📒 Files selected for processing (1)
  • packages/stream_chat_flutter/test/src/mocks.dart

📝 Walkthrough

Walkthrough

The PR adds localized accessibility semantics for message previews, drafts, avatars, timestamps, sending indicators, and channel/thread list tiles. It introduces formatter and translation APIs, wires labels into widgets, updates supported locales, adds coverage, and pins stream_core_flutter to a new commit.

Changes

Accessibility semantics

Layer / File(s) Summary
Accessibility contracts and preview formatting
packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart, packages/stream_chat_flutter/lib/src/localization/accessibility_translations.dart
Adds semantic-label formatter APIs and localized handling for messages, drafts, attachments, statuses, unread counts, and recent timestamps.
Localized accessibility implementations
packages/stream_chat_localizations/lib/src/stream_chat_localizations_*.dart
Adds accessibility labels, pluralized announcements, tooltip wording, and recent-date formatting across supported locales.
Widget semantics wiring
packages/stream_chat_flutter/lib/src/channel/*, lib/src/components/avatar/*, lib/src/misc/timestamp.dart, lib/src/scroll_view/*
Passes semantic labels to previews, avatars, timestamps, and indicators; merges list-tile semantics and excludes decorative content.
Accessibility validation and package wiring
packages/stream_chat_flutter/test/**, packages/stream_chat_localizations/test/**, **/CHANGELOG.md, pubspec.yaml, melos.yaml
Adds semantics assertions, updates accessibility changelogs, and changes the pinned stream_core_flutter commit.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChannelListTile
  participant MessagePreviewFormatter
  participant AccessibilityTranslations
  participant FlutterSemantics
  ChannelListTile->>MessagePreviewFormatter: format message or draft label
  MessagePreviewFormatter->>AccessibilityTranslations: resolve localized labels
  AccessibilityTranslations-->>MessagePreviewFormatter: return formatted text
  MessagePreviewFormatter-->>ChannelListTile: provide semanticsLabel
  ChannelListTile->>FlutterSemantics: merge tile, avatar, preview, and timestamp semantics
Loading

Possibly related PRs

Suggested reviewers: renefloor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main accessibility-focused changes to channel lists and message previews.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/a11y-fixes-batch-2

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.

xsahil03x and others added 3 commits July 21, 2026 00:13
# Conflicts:
#	packages/stream_chat_flutter/CHANGELOG.md
Points the git dep at c711bba, the commit on the open
GetStream/stream-core-flutter#132 branch that adds `semanticsLabel` to
`StreamAvatar` / `StreamAvatarGroup` / `StreamAvatarStack`. Required
so our chat-side widgets compile; must be swapped to the merged main
ref once #132 lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, screen readers walked the thread-list row fragment by
fragment — announcing avatar initial, channel name, sender + message,
reply count, and timestamp as separate stops. This commit wraps the
tile in `MergeSemantics` so the row exposes as a single node with one
composed label, and labels the unread badge via
`AccessibilityTranslations.unreadMessagesLabel(count)`.

Fixes FLU-591.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x marked this pull request as ready for review July 20, 2026 23:25
xsahil03x and others added 2 commits July 21, 2026 01:28
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/stream_chat_flutter/lib/src/localization/accessibility_translations.dart (1)

599-614: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Default time format diverges from every locale implementation.

DefaultAccessibilityTranslations.formatRecentDateTime formats the time with jiffyDate.format(pattern: 'H:mm') (24‑hour, matching the interface doc examples like "Today at 14:30"), but all locale implementations (_AccessibilityTranslationsEn, _ca, _de, _es, _fr, _hi, _it) use jiffyDate.jm, which renders 12‑hour 2:30 PM for English. Since DefaultTranslations.accessibility returns this default while StreamChatLocalizationsEn returns _AccessibilityTranslationsEn, an English user hears a different timestamp format depending on whether stream_chat_localizations is wired up.

Consider aligning the default with the locale bundles (use jiffyDate.jm) or vice‑versa so the spoken timestamp is consistent.

♻️ Align default with locale implementations
-    final time = jiffyDate.format(pattern: 'H:mm');
+    final time = jiffyDate.jm;
🤖 Prompt for 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.

In
`@packages/stream_chat_flutter/lib/src/localization/accessibility_translations.dart`
around lines 599 - 614, Update
DefaultAccessibilityTranslations.formatRecentDateTime to use the same time
formatting as the locale implementations, replacing the 24-hour jiffyDate format
with the shared 12-hour jiffyDate.jm representation while preserving all
existing date-label branches.
🤖 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.

Nitpick comments:
In
`@packages/stream_chat_flutter/lib/src/localization/accessibility_translations.dart`:
- Around line 599-614: Update
DefaultAccessibilityTranslations.formatRecentDateTime to use the same time
formatting as the locale implementations, replacing the 24-hour jiffyDate format
with the shared 12-hour jiffyDate.jm representation while preserving all
existing date-label branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1eb1478d-104d-47fd-bbe2-c60a28d57b61

📥 Commits

Reviewing files that changed from the base of the PR and between 4dee989 and a20b8e6.

📒 Files selected for processing (36)
  • docs/docs_screenshots/pubspec.yaml
  • melos.yaml
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/channel/stream_draft_message_preview_text.dart
  • packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart
  • packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart
  • packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart
  • packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart
  • packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_stack.dart
  • packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart
  • packages/stream_chat_flutter/lib/src/localization/accessibility_translations.dart
  • packages/stream_chat_flutter/lib/src/localization/translations.dart
  • packages/stream_chat_flutter/lib/src/misc/timestamp.dart
  • packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart
  • packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart
  • packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart
  • packages/stream_chat_flutter/pubspec.yaml
  • packages/stream_chat_flutter/test/src/channel/stream_draft_message_preview_text_test.dart
  • packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart
  • packages/stream_chat_flutter/test/src/misc/timestamp_test.dart
  • packages/stream_chat_flutter/test/src/scroll_view/channel_scroll_view/stream_channel_list_item_test.dart
  • packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart
  • packages/stream_chat_localizations/CHANGELOG.md
  • packages/stream_chat_localizations/example/lib/add_new_lang.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart
  • packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart
  • packages/stream_chat_localizations/test/translations_test.dart

`StreamChannelAvatar` now reads `channel.isGroup` / `channel.isOneToOne`
to pick its default `semanticsLabel`; mocktail returns `null` for
un-stubbed bool getters, which fails a runtime type check. Register
both as `false` in the base `MockChannel` so existing test files that
don't care about a11y keep passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.46835% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.19%. Comparing base (4dee989) to head (c8ee497).

Files with missing lines Patch % Lines
...b/src/localization/accessibility_translations.dart 75.00% 9 Missing ⚠️
...src/channel/stream_draft_message_preview_text.dart 75.00% 1 Missing ⚠️
...r/lib/src/channel/stream_message_preview_text.dart 75.00% 1 Missing ⚠️
...b/src/components/avatar/stream_channel_avatar.dart 80.00% 1 Missing ⚠️
.../lib/src/components/avatar/stream_user_avatar.dart 75.00% 1 Missing ⚠️
.../channel_scroll_view/stream_channel_list_item.dart 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2827      +/-   ##
==========================================
+ Coverage   71.35%   72.19%   +0.83%     
==========================================
  Files         430      430              
  Lines       26945    27449     +504     
==========================================
+ Hits        19226    19816     +590     
+ Misses       7719     7633      -86     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant