Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs_screenshots/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 7d0421a2cc04480e0bd2fc404cf6dd5b051f7f42
ref: c711bba905ac38bd81fdf0365ef1731323d61146
path: packages/stream_core_flutter

dev_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ command:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 7d0421a2cc04480e0bd2fc404cf6dd5b051f7f42
ref: c711bba905ac38bd81fdf0365ef1731323d61146
path: packages/stream_core_flutter
synchronized: ^3.4.0
thumblr: ^0.0.4
Expand Down
10 changes: 9 additions & 1 deletion packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

✅ Added

- Added an `AccessibilityTranslations` namespace on `Translations`, accessed via `context.translations.accessibility`, holding all screen-reader labels, tooltips, hints, and live-region announcements used by the composer, voice recording, attachment picker, message actions, channel header, media gallery, and poll creator. Getter suffixes follow Flutter's `MaterialLocalizations` convention (`Tooltip`, `Label`, `Hint`, `TapHint`, `Announcement`). Added a `DateTime.toA11yTimestamp()` extension for locale-aware long-form timestamps in accessibility labels.
- Added `AccessibilityTranslations` on `Translations` (accessed via `context.translations.accessibility`) — screen-reader labels, tooltips, hints, and live-region announcements. Includes `formatDateTime`, `formatDuration`, and `formatRecentDateTime` formatters.
- Added `AccessibleMessagePreviewFormatter` — an optional interface extending `MessagePreviewFormatter` with `formatMessageSemanticsLabel` and `formatDraftMessageSemanticsLabel`. `StreamMessagePreviewText`, `StreamDraftMessagePreviewText`, and `StreamSendingIndicator` now emit natural-language screen-reader labels for the channel list.
- Added optional `semanticsLabel` to `StreamChannelAvatar`. Group channels emit a default `"Group"` label; direct-message avatars stay silent.
- Added optional `semanticsLabel` to `StreamTimestamp`. Defaults to a bucketed natural-language phrasing via `AccessibilityTranslations.formatRecentDateTime`.
- Added a `LastMessagePredicate` typedef for the `ChannelLastMessageText.lastMessagePredicate` filter.
- Added optional `semanticsLabel` to `StreamUserAvatar`, `StreamUserAvatarGroup`, and `StreamUserAvatarStack`.

🐞 Fixed

Expand All @@ -12,6 +16,10 @@
- Fixed `StreamMessageListView` firing `markThreadRead` on a reply-less parent, which produced a guaranteed 404 every time the thread view was opened before the first reply.
- Fixed dismissing the `StreamMessageListView` unread indicator being ignored while a channel is receiving a rapid burst of messages.
- Fixed `StreamTypingIndicator` rebuilding on every typing event by comparing the typing users by id, so it only rebuilds when the set of typing users changes.
- Fixed `StreamChannelListTile` announcing avatar initials and unlabeled fragments — now merges into a single accessible row summary with labeled avatar, muted / pinned icons, and unread badge.
- Fixed `StreamThreadListTile` announcing content as flat comma-separated fragments — now merges into a single accessible row summary and labels the unread badge with the localized `"N unread messages"` phrasing.
- Renamed `attachmentPickerTooltip` to state-agnostic "Toggle attachment picker" so screen readers no longer announce "Open attachment picker" while the picker is already expanded.
- Fixed `voiceRecordingText` casing to sentence case (`"Voice recording"`).

## 10.1.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,26 @@ class StreamDraftMessagePreviewText extends StatelessWidget {
showCaption: showCaption,
);

// Prefer a hand-crafted a11y label when the formatter opts into
// [AccessibleMessagePreviewFormatter]; fall back to the visual
// TextSpan stripped of inline icon placeholders.
final a11yLabel = switch (formatter) {
final AccessibleMessagePreviewFormatter it => it.formatDraftMessageSemanticsLabel(
context,
draftMessage,
currentUser: currentUser,
showCaption: showCaption,
),
_ => previewTextSpan.toPlainText(includePlaceholders: false),
};

return Text.rich(
maxLines: 1,
previewTextSpan,
style: textStyle,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
semanticsLabel: a11yLabel,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,29 @@ class StreamMessagePreviewText extends StatelessWidget {
showCaption: showCaption,
);

// Prefer a hand-crafted a11y label when the formatter opts into
// [AccessibleMessagePreviewFormatter]; fall back to the visual
// TextSpan stripped of inline icon placeholders so custom formatters
// that only implement [MessagePreviewFormatter] still get a
// reasonable — if less rich — screen-reader announcement.
final a11yLabel = switch (formatter) {
final AccessibleMessagePreviewFormatter it => it.formatMessageSemanticsLabel(
context,
previewMessage,
channel: channel,
currentUser: currentUser,
showCaption: showCaption,
),
_ => previewTextSpan.toPlainText(includePlaceholders: false),
};

return Text.rich(
maxLines: 1,
previewTextSpan,
style: textStyle,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
semanticsLabel: a11yLabel,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart';
import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar_group.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_core_flutter/chat.dart';

Expand Down Expand Up @@ -54,6 +55,7 @@ class StreamChannelAvatar extends StatelessWidget {
super.key,
this.size,
required this.channel,
this.semanticsLabel,
});

/// The channel whose avatar is displayed.
Expand All @@ -64,17 +66,27 @@ class StreamChannelAvatar extends StatelessWidget {
/// If null, defaults to [StreamAvatarGroupSize.lg].
final StreamAvatarGroupSize? size;

/// Screen-reader label for the avatar.
///
/// When null (the default), a `"Group"` label is emitted for group
/// channels and direct-message avatars stay silent. When non-null, the
/// given string replaces the default; an empty string leaves the avatar
/// silent.
final String? semanticsLabel;

@override
Widget build(BuildContext context) {
assert(channel.state != null, 'Channel ${channel.id} is not initialized');

final effectiveSize = size ?? StreamAvatarGroupSize.lg;
final effectiveLabel = semanticsLabel ?? _defaultSemanticsLabel(context);

return BetterStreamBuilder(
stream: channel.imageStream,
initialData: channel.image,
builder: (context, channelImage) => StreamAvatar(
imageUrl: channelImage,
semanticsLabel: effectiveLabel,
size: _avatarSizeForAvatarGroupSize(effectiveSize),
placeholder: (_) => const _StreamChannelAvatarPlaceholder(),
),
Expand All @@ -85,13 +97,14 @@ class StreamChannelAvatar extends StatelessWidget {
final users = members.map((it) => it.user!).toList();
final currentUserId = channel.client.state.currentUser?.id;

if (channel.isDistinct && users.length == 2) {
if (channel.isOneToOne) {
final otherUser = users.firstWhere(
(u) => u.id != currentUserId,
orElse: () => users.first,
);
return StreamUserAvatar(
user: otherUser,
semanticsLabel: effectiveLabel,
size: _avatarSizeForAvatarGroupSize(effectiveSize),
// TODO: make this configurable when the online state is shown.
showOnlineIndicator: otherUser.online,
Expand All @@ -100,13 +113,23 @@ class StreamChannelAvatar extends StatelessWidget {

return StreamUserAvatarGroup(
size: effectiveSize,
semanticsLabel: effectiveLabel,
users: users.sortedBy((it) => it.id == currentUserId ? 1 : 0),
);
},
),
);
}

// Computes the default screen-reader label for the avatar based on the
// channel type. Returns "Group" for group channels; direct-message
// avatars stay silent so the counterpart's identity — announced by the
// row's title — isn't duplicated.
String? _defaultSemanticsLabel(BuildContext context) {
if (!channel.isGroup) return null;
return context.translations.accessibility.channelGroupLabel;
}

// Maps [StreamAvatarGroupSize] to corresponding [StreamAvatarSize].
//
// Used when displaying a single channel image avatar.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class StreamUserAvatar extends StatelessWidget {
required this.user,
this.showBorder = true,
this.showOnlineIndicator = true,
this.semanticsLabel,
});

/// The user whose avatar is displayed.
Expand All @@ -90,6 +91,12 @@ class StreamUserAvatar extends StatelessWidget {
/// [StreamAvatarSize.lg].
final StreamAvatarSize? size;

/// Screen-reader label for the avatar.
///
/// When null (the default), the avatar is silent. When non-null, the
/// avatar is exposed as a labeled image node.
final String? semanticsLabel;

@override
Widget build(BuildContext context) {
final avatarTheme = context.streamAvatarTheme;
Expand All @@ -109,6 +116,7 @@ class StreamUserAvatar extends StatelessWidget {
showBorder: showBorder,
backgroundColor: effectiveBackgroundColor,
foregroundColor: effectiveForegroundColor,
semanticsLabel: semanticsLabel,
placeholder: (_) => _StreamUserAvatarPlaceholder(user: user, size: effectiveSize),
);

Expand Down Expand Up @@ -145,6 +153,12 @@ class StreamUserAvatar extends StatelessWidget {
// Displays user initials or a fallback person icon when no name is available.
// Shows full initials (up to 2 characters) for medium and larger sizes,
// and only the first initial for extra-small and small sizes.
//
// The initials are decorative — a visual identifier for sighted users, not
// information a screen reader can act on. Wrapped in [ExcludeSemantics] so
// consumers who leave [StreamUserAvatar.semanticsLabel] null get a silent
// avatar by default; passing a `semanticsLabel` re-emits a labeled image
// node via the base [StreamAvatar]'s semantics wrap.
class _StreamUserAvatarPlaceholder extends StatelessWidget {
const _StreamUserAvatarPlaceholder({
required this.user,
Expand All @@ -157,13 +171,16 @@ class _StreamUserAvatarPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userInitials = user.name.initials;
final Widget content;
if (userInitials != null && userInitials.isNotEmpty) {
return switch (size) {
content = switch (size) {
.md || .lg || .xl || .xxl => Text(userInitials),
.xs || .sm => Text(userInitials.characters.first),
};
} else {
content = Icon(context.streamIcons.user);
}

return Icon(context.streamIcons.user);
return ExcludeSemantics(child: content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class StreamUserAvatarGroup extends StatelessWidget {
super.key,
required this.users,
this.size,
this.semanticsLabel,
});

/// The list of users whose avatars are displayed.
Expand All @@ -65,10 +66,18 @@ class StreamUserAvatarGroup extends StatelessWidget {
/// If null, defaults to [StreamAvatarGroupSize.lg].
final StreamAvatarGroupSize? size;

/// Screen-reader label for the avatar group.
///
/// When null (the default), each child avatar carries its own
/// [StreamUserAvatar.semanticsLabel]. When non-null, the group is exposed
/// as a single labeled image node.
final String? semanticsLabel;

@override
Widget build(BuildContext context) {
return StreamAvatarGroup(
size: size,
semanticsLabel: semanticsLabel,
children: users.map(
(user) => StreamUserAvatar(
user: user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class StreamUserAvatarStack extends StatelessWidget {
this.size,
this.overlap = 0.33,
this.max = 5,
this.semanticsLabel,
}) : assert(max >= 2, 'max must be at least 2');

/// The list of users whose avatars are displayed.
Expand All @@ -96,12 +97,20 @@ class StreamUserAvatarStack extends StatelessWidget {
/// Must be at least 2. Defaults to 5.
final int max;

/// Screen-reader label for the avatar stack.
///
/// When null (the default), each child avatar carries its own
/// [StreamUserAvatar.semanticsLabel]. When non-null, the stack is exposed
/// as a single labeled image node.
final String? semanticsLabel;

@override
Widget build(BuildContext context) {
return StreamAvatarStack(
max: max,
size: size,
overlap: overlap,
semanticsLabel: semanticsLabel,
children: users.map(
(user) => StreamUserAvatar(
user: user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ class StreamSendingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = context.streamColorScheme;
final a11y = context.translations.accessibility;

if (isMessageRead) {
return Icon(
context.streamIcons.checks,
size: size,
color: colorScheme.accentPrimary,
semanticLabel: a11y.messageReadStatusLabel,
);
}

Expand All @@ -44,6 +46,7 @@ class StreamSendingIndicator extends StatelessWidget {
context.streamIcons.checks,
size: size,
color: colorScheme.textSecondary,
semanticLabel: a11y.messageDeliveredStatusLabel,
);
}

Expand All @@ -52,6 +55,7 @@ class StreamSendingIndicator extends StatelessWidget {
context.streamIcons.checkmark,
size: size,
color: colorScheme.textSecondary,
semanticLabel: a11y.messageSentStatusLabel,
);
}

Expand All @@ -60,6 +64,7 @@ class StreamSendingIndicator extends StatelessWidget {
context.streamIcons.clock,
size: size,
color: colorScheme.textSecondary,
semanticLabel: a11y.messageSendingStatusLabel,
);
}

Expand Down
Loading
Loading