feat: add semantics for texts

This commit is contained in:
Jacques 2025-02-27 17:25:20 +01:00 committed by FlutterJoey
parent 6ecf073f15
commit 30fc7b4368
13 changed files with 456 additions and 125 deletions

View file

@ -169,6 +169,9 @@ typedef ChatMessageBuilder = Widget? Function(
MessageModel? previousMessage,
UserModel? sender,
Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
);
/// The group avatar builder

View file

@ -2,6 +2,7 @@ import "package:cached_network_image/cached_network_image.dart";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_builders.dart";
import "package:flutter_chat/src/config/chat_semantics.dart";
import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat options
@ -13,6 +14,7 @@ class ChatOptions {
this.groupChatEnabled = true,
this.enableLoadingIndicator = true,
this.translations = const ChatTranslations.empty(),
this.semantics = const ChatSemantics.standard(),
this.builders = const ChatBuilders(),
this.spacing = const ChatSpacing(),
this.paginationControls = const ChatPaginationControls(),
@ -43,6 +45,9 @@ class ChatOptions {
/// [translations] is the chat translations.
final ChatTranslations translations;
/// [semantics] is the chat semantics.
final ChatSemantics semantics;
/// [builders] is the chat builders.
final ChatBuilders builders;

View file

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: public_member_api_docs
/// Class that holds all the semantic ids for the chat component view and
/// the corresponding userstory
class ChatSemantics {
/// ChatTranslations constructor where everything is required use this
/// if you want to be sure to have all translations specified
/// If you just want the default values use the empty constructor
/// and optionally override the values with the copyWith method
const ChatSemantics({
required this.profileTitle,
required this.profileDescription,
required this.chatUnreadMessages,
required this.chatChatTitle,
required this.chatNoMessages,
required this.newChatGetUsersError,
required this.newGroupChatMemberAmount,
required this.newGroupChatGetUsersError,
required this.newChatUserListUserFullName,
required this.chatBubbleTitle,
required this.chatBubbleTime,
required this.chatBubbleText,
required this.chatsChatTitle,
required this.chatsChatSubTitle,
required this.chatsChatLastUsed,
required this.chatsChatUnreadMessages,
});
/// Default translations for the chat component view
const ChatSemantics.standard({
this.profileTitle = "text_profile_title",
this.profileDescription = "text_profile_description",
this.chatUnreadMessages = "text_unread_messages",
this.chatChatTitle = "text_chat_title",
this.chatNoMessages = "text_no_messages",
this.newChatGetUsersError = "text_get_users_error",
this.newGroupChatMemberAmount = "text_member_amount",
this.newGroupChatGetUsersError = "text_get_users_error",
this.newChatUserListUserFullName = _defaultNewChatUserListUserFullName,
this.chatBubbleTitle = _defaultChatBubbleTitle,
this.chatBubbleTime = _defaultChatBubbleTime,
this.chatBubbleText = _defaultChatBubbleText,
this.chatsChatTitle = _defaultChatsChatTitle,
this.chatsChatSubTitle = _defaultChatsChatSubTitle,
this.chatsChatLastUsed = _defaultChatsChatLastUsed,
this.chatsChatUnreadMessages = _defaultChatsChatUnreadMessages,
});
// Text
final String profileTitle;
final String profileDescription;
final String chatUnreadMessages;
final String chatChatTitle;
final String chatNoMessages;
final String newChatGetUsersError;
final String newGroupChatMemberAmount;
final String newGroupChatGetUsersError;
// Indexed text
final String Function(int index) newChatUserListUserFullName;
final String Function(int index) chatBubbleTitle;
final String Function(int index) chatBubbleTime;
final String Function(int index) chatBubbleText;
final String Function(int index) chatsChatTitle;
final String Function(int index) chatsChatSubTitle;
final String Function(int index) chatsChatLastUsed;
final String Function(int index) chatsChatUnreadMessages;
ChatSemantics copyWith({
String? profileTitle,
String? profileDescription,
String? chatUnreadMessages,
String? chatChatTitle,
String? chatNoMessages,
String? newChatGetUsersError,
String? newGroupChatMemberAmount,
String? newGroupChatGetUsersError,
String Function(int)? newChatUserListUserFullName,
String Function(int)? chatBubbleTitle,
String Function(int)? chatBubbleTime,
String Function(int)? chatBubbleText,
String Function(int)? chatsChatTitle,
String Function(int)? chatsChatSubTitle,
String Function(int)? chatsChatLastUsed,
String Function(int)? chatsChatUnreadMessages,
}) =>
ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle,
profileDescription: profileDescription ?? this.profileDescription,
chatUnreadMessages: chatUnreadMessages ?? this.chatUnreadMessages,
chatChatTitle: chatChatTitle ?? this.chatChatTitle,
chatNoMessages: chatNoMessages ?? this.chatNoMessages,
newChatGetUsersError: newChatGetUsersError ?? this.newChatGetUsersError,
newGroupChatMemberAmount:
newGroupChatMemberAmount ?? this.newGroupChatMemberAmount,
newGroupChatGetUsersError:
newGroupChatGetUsersError ?? this.newGroupChatGetUsersError,
newChatUserListUserFullName:
newChatUserListUserFullName ?? this.newChatUserListUserFullName,
chatBubbleTitle: chatBubbleTitle ?? this.chatBubbleTitle,
chatBubbleTime: chatBubbleTime ?? this.chatBubbleTime,
chatBubbleText: chatBubbleText ?? this.chatBubbleText,
chatsChatTitle: chatsChatTitle ?? this.chatsChatTitle,
chatsChatSubTitle: chatsChatSubTitle ?? this.chatsChatSubTitle,
chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed,
chatsChatUnreadMessages:
chatsChatUnreadMessages ?? this.chatsChatUnreadMessages,
);
}
String _defaultNewChatUserListUserFullName(int index) =>
"text_user_fullname_$index";
String _defaultChatBubbleTitle(int index) => "text_chat_bubble_title_$index";
String _defaultChatBubbleTime(int index) => "text_chat_bubble_time_$index";
String _defaultChatBubbleText(int index) => "text_chat_bubble_text_$index";
String _defaultChatsChatTitle(int index) => "text_chat_title_$index";
String _defaultChatsChatSubTitle(int index) => "text_chat_sub_title_$index";
String _defaultChatsChatLastUsed(int index) => "text_chat_last_used_$index";
String _defaultChatsChatUnreadMessages(int index) =>
"text_chat_unread_messages_$index";

View file

@ -1,6 +1,7 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/flutter_chat.dart";
/// A widget representing an entry point for a chat UI.
@ -16,6 +17,7 @@ class FlutterChatEntryWidget extends StatefulWidget {
this.iconColor = Colors.black,
this.counterBackgroundColor = Colors.red,
this.textStyle,
this.semanticIdUnreadMessages = "text_unread_messages_count",
super.key,
});
@ -46,6 +48,9 @@ class FlutterChatEntryWidget extends StatefulWidget {
/// The chat options
final ChatOptions? options;
/// Semantic Id for the unread messages text
final String semanticIdUnreadMessages;
@override
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
}
@ -121,9 +126,13 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
color: widget.counterBackgroundColor,
),
child: Center(
child: Text(
snapshot.data?.toString() ?? "0",
style: widget.textStyle,
child: CustomSemantics(
identifier: widget.semanticIdUnreadMessages,
value: snapshot.data?.toString() ?? "0",
child: Text(
snapshot.data?.toString() ?? "0",
style: widget.textStyle,
),
),
),
),

View file

@ -4,6 +4,7 @@ import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_bottom.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.dart";
@ -213,11 +214,15 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: onPressChatTitle,
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
child: CustomSemantics(
identifier: options.semantics.chatChatTitle,
value: chatTitle ?? "",
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
),
);
}
@ -458,6 +463,9 @@ class _ChatBody extends HookWidget {
previousMessage: prevMsg,
sender: userMap[msg.senderId],
onPressSender: onPressUserProfile,
semanticIdTitle: options.semantics.chatBubbleTitle(index),
semanticIdTime: options.semantics.chatBubbleTime(index),
semanticIdText: options.semantics.chatBubbleText(index),
),
);
}

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/default_message_builder.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
@ -18,15 +19,22 @@ class ChatNoMessages extends HookWidget {
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var translations = chatScope.options.translations;
var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context);
return Center(
child: Text(
isGroupChat
child: CustomSemantics(
identifier: options.semantics.chatNoMessages,
value: isGroupChat
? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat,
style: theme.textTheme.bodySmall,
child: Text(
isGroupChat
? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat,
style: theme.textTheme.bodySmall,
),
),
);
}
@ -39,6 +47,9 @@ class ChatBubble extends HookWidget {
required this.message,
required this.sender,
required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
this.previousMessage,
super.key,
});
@ -56,6 +67,15 @@ class ChatBubble extends HookWidget {
/// Callback function when a message sender is pressed.
final Function(UserModel user) onPressSender;
/// Semantic id for message title
final String semanticIdTitle;
/// Semantic id for message time
final String semanticIdTime;
/// Semantic id for message text
final String semanticIdText;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
@ -67,12 +87,18 @@ class ChatBubble extends HookWidget {
previousMessage,
sender,
onPressSender,
semanticIdTitle,
semanticIdTime,
semanticIdText,
) ??
DefaultChatMessageBuilder(
message: message,
previousMessage: previousMessage,
sender: sender,
onPressSender: onPressSender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
}
}

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_chat/src/util/scope.dart";
@ -14,6 +15,9 @@ class DefaultChatMessageBuilder extends StatelessWidget {
required this.previousMessage,
required this.sender,
required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
super.key,
});
@ -30,6 +34,15 @@ class DefaultChatMessageBuilder extends StatelessWidget {
/// The function that is called when the sender is clicked
final Function(UserModel user) onPressSender;
/// Semantic id for message title
final String semanticIdTitle;
/// Semantic id for message time
final String semanticIdTime;
/// Semantic id for message text
final String semanticIdText;
/// implements [ChatMessageBuilder]
static Widget builder(
BuildContext context,
@ -37,12 +50,18 @@ class DefaultChatMessageBuilder extends StatelessWidget {
MessageModel? previousMessage,
UserModel? sender,
Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
) =>
DefaultChatMessageBuilder(
message: message,
previousMessage: previousMessage,
sender: sender,
onPressSender: onPressSender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
/// Merges the [MessageTheme] from the themeresolver with the [MessageTheme]
@ -87,6 +106,9 @@ class DefaultChatMessageBuilder extends StatelessWidget {
message: message,
messageTheme: messageTheme,
sender: sender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
var messagePadding = messageTheme.messageSidePadding!;
@ -126,6 +148,9 @@ class _ChatMessageBubble extends StatelessWidget {
required this.previousMessage,
required this.messageTheme,
required this.sender,
required this.semanticIdTitle,
required this.semanticIdTime,
required this.semanticIdText,
});
final bool isSameSender;
@ -134,6 +159,9 @@ class _ChatMessageBubble extends StatelessWidget {
final MessageModel? previousMessage;
final MessageTheme messageTheme;
final UserModel? sender;
final String semanticIdTitle;
final String semanticIdTime;
final String semanticIdText;
@override
Widget build(BuildContext context) {
@ -154,9 +182,13 @@ class _ChatMessageBubble extends StatelessWidget {
var senderTitle =
options.senderTitleResolver?.call(sender) ?? sender?.firstName ?? "";
var senderTitleText = Text(
senderTitle,
style: theme.textTheme.titleMedium,
var senderTitleText = CustomSemantics(
identifier: semanticIdTitle,
value: senderTitle,
child: Text(
senderTitle,
style: theme.textTheme.titleMedium,
),
);
var messageTimeRow = Row(
@ -164,12 +196,16 @@ class _ChatMessageBubble extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.only(right: 8, bottom: 4),
child: Text(
messageTime,
style: textTheme.bodySmall?.copyWith(
color: messageTheme.textColor,
child: CustomSemantics(
identifier: semanticIdTime,
value: messageTime,
child: Text(
messageTime,
style: textTheme.bodySmall?.copyWith(
color: messageTheme.textColor,
),
textAlign: TextAlign.end,
),
textAlign: TextAlign.end,
),
),
],
@ -207,12 +243,16 @@ class _ChatMessageBubble extends StatelessWidget {
right: 12,
bottom: 4,
),
child: Text(
message.text!,
style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor,
child: Semantics(
identifier: semanticIdText,
value: message.text,
child: Text(
message.text!,
style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor,
),
textAlign: messageTheme.textAlignment,
),
textAlign: messageTheme.textAlignment,
),
),
],
@ -257,6 +297,7 @@ class _DefaultChatImage extends StatelessWidget {
options.imageProviderResolver(context, Uri.parse(imageUrl)),
fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) => Text(
// TODO: Non-replaceable text
"Something went wrong with loading the image",
style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor,

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
@ -50,7 +51,10 @@ class ChatProfileScreen extends HookWidget {
? chatModel?.chatName ?? options.translations.groupNameEmpty
: "";
var appBar = _AppBar(title: chatTitle);
var appBar = _AppBar(
title: chatTitle,
semanticId: options.semantics.profileTitle,
);
var body = _Body(
user: userModel,
@ -79,16 +83,22 @@ class ChatProfileScreen extends HookWidget {
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.title,
required this.semanticId,
});
final String title;
final String semanticId;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme,
title: Text(title),
title: CustomSemantics(
identifier: semanticId,
value: title,
child: Text(title),
),
);
}
@ -247,9 +257,13 @@ class _Body extends StatelessWidget {
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 12),
Text(
chat!.description ?? "",
style: theme.textTheme.bodyMedium,
CustomSemantics(
identifier: options.semantics.profileDescription,
value: chat!.description ?? "",
child: Text(
chat!.description ?? "",
style: theme.textTheme.bodyMedium,
),
),
const SizedBox(height: 12),
Text(

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/chat_translations.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/services/date_formatter.dart";
@ -92,9 +93,13 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
visible: (snapshot.data ?? 0) > 0,
child: Padding(
padding: const EdgeInsets.only(right: 22.0),
child: Text(
"${snapshot.data ?? 0} ${translations.chatsUnread}",
style: theme.textTheme.bodySmall,
child: CustomSemantics(
identifier: options.semantics.chatUnreadMessages,
value: "${snapshot.data ?? 0} ${translations.chatsUnread}",
child: Text(
"${snapshot.data ?? 0} ${translations.chatsUnread}",
style: theme.textTheme.bodySmall,
),
),
),
),
@ -166,7 +171,8 @@ class _BodyState extends State<_Body> {
}
return Column(
children: [
for (ChatModel chat in snapshot.data ?? []) ...[
for (var (index, ChatModel chat)
in (snapshot.data ?? []).indexed) ...[
DecoratedBox(
decoration: BoxDecoration(
border: Border(
@ -177,66 +183,77 @@ class _BodyState extends State<_Body> {
),
),
child: Builder(
builder: (context) => !chat.canBeDeleted
? Dismissible(
confirmDismiss: (_) async {
await options
.builders.deleteChatDialogBuilder
?.call(context, chat) ??
_deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
return _deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
},
onDismissed: (_) {
widget.onDeleteChat(chat);
},
secondaryBackground: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
builder: (context) {
var semantics = options.semantics;
var chatItem = _ChatItem(
chat: chat,
onPressChat: widget.onPressChat,
semanticIdTitle:
semantics.chatsChatTitle(index),
semanticIdSubTitle:
semantics.chatsChatSubTitle(index),
semanticIdLastUsed:
semantics.chatsChatLastUsed(index),
semanticIdUnreadMessages:
semantics.chatsChatUnreadMessages(index),
);
return !chat.canBeDeleted
? Dismissible(
confirmDismiss: (_) async {
await options.builders
.deleteChatDialogBuilder
?.call(context, chat) ??
_deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
return _deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
},
onDismissed: (_) {
widget.onDeleteChat(chat);
},
secondaryBackground: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
),
background: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
background: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
),
key: ValueKey(
chat.id,
),
child: _ChatItem(
chat: chat,
onPressChat: widget.onPressChat,
),
)
: _ChatItem(
chat: chat,
onPressChat: widget.onPressChat,
),
key: ValueKey(
chat.id,
),
child: chatItem,
)
: chatItem;
},
),
),
],
@ -282,10 +299,18 @@ class _ChatItem extends StatelessWidget {
const _ChatItem({
required this.chat,
required this.onPressChat,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
});
final ChatModel chat;
final Function(ChatModel chat) onPressChat;
final String semanticIdTitle;
final String semanticIdSubTitle;
final String semanticIdLastUsed;
final String semanticIdUnreadMessages;
@override
Widget build(BuildContext context) {
@ -295,16 +320,23 @@ class _ChatItem extends StatelessWidget {
options: options,
);
var theme = Theme.of(context);
var chatListItem = _ChatListItem(
chat: chat,
dateFormatter: dateFormatter,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
);
return InkWell(
onTap: () {
onPressChat(chat);
},
child: options.builders.chatRowContainerBuilder?.call(
context,
_ChatListItem(
chat: chat,
dateFormatter: dateFormatter,
),
chatListItem,
) ??
DecoratedBox(
decoration: BoxDecoration(
@ -318,10 +350,7 @@ class _ChatItem extends StatelessWidget {
),
child: Padding(
padding: const EdgeInsets.all(12),
child: _ChatListItem(
chat: chat,
dateFormatter: dateFormatter,
),
child: chatListItem,
),
),
);
@ -332,10 +361,18 @@ class _ChatListItem extends StatelessWidget {
const _ChatListItem({
required this.chat,
required this.dateFormatter,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
});
final ChatModel chat;
final DateFormatter dateFormatter;
final String semanticIdTitle;
final String semanticIdSubTitle;
final String semanticIdLastUsed;
final String semanticIdUnreadMessages;
@override
Widget build(BuildContext context) {
@ -366,6 +403,10 @@ class _ChatListItem extends StatelessWidget {
return _ChatRow(
title: chat.chatName ?? translations.groupNameEmpty,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
unreadMessages:
showUnreadMessageCount ? chat.unreadMessageCount : 0,
subTitle: data != null
@ -441,6 +482,10 @@ class _ChatListItem extends StatelessWidget {
return _ChatRow(
unreadMessages:
showUnreadMessageCount ? chat.unreadMessageCount : 0,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
avatar: options.builders.userAvatarBuilder?.call(
context,
otherUser,
@ -536,6 +581,10 @@ Future<bool?> _deleteDialog(
class _ChatRow extends StatelessWidget {
const _ChatRow({
required this.title,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
this.unreadMessages = 0,
this.lastUsed,
this.subTitle,
@ -544,15 +593,19 @@ class _ChatRow extends StatelessWidget {
/// The title of the chat.
final String title;
final String semanticIdTitle;
/// The number of unread messages in the chat.
final int unreadMessages;
final String semanticIdUnreadMessages;
/// The last time the chat was used.
final String? lastUsed;
final String semanticIdLastUsed;
/// The subtitle of the chat.
final String? subTitle;
final String semanticIdSubTitle;
/// The avatar associated with the chat.
final Widget? avatar;
@ -573,20 +626,28 @@ class _ChatRow extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium,
CustomSemantics(
identifier: semanticIdTitle,
value: title,
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium,
),
),
if (subTitle != null) ...[
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: Text(
subTitle!,
style: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 2,
child: CustomSemantics(
identifier: semanticIdSubTitle,
value: subTitle,
child: Text(
subTitle!,
style: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
),
],
@ -601,9 +662,13 @@ class _ChatRow extends StatelessWidget {
if (lastUsed != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
lastUsed!,
style: theme.textTheme.labelSmall,
child: CustomSemantics(
identifier: semanticIdLastUsed,
value: lastUsed,
child: Text(
lastUsed!,
style: theme.textTheme.labelSmall,
),
),
),
],
@ -616,10 +681,14 @@ class _ChatRow extends StatelessWidget {
shape: BoxShape.circle,
),
child: Center(
child: Text(
unreadMessages.toString(),
style: const TextStyle(
fontSize: 14,
child: CustomSemantics(
identifier: semanticIdUnreadMessages,
value: unreadMessages.toString(),
child: Text(
unreadMessages.toString(),
style: const TextStyle(
fontSize: 14,
),
),
),
),

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
@ -211,10 +212,17 @@ class _Body extends StatelessWidget {
// ignore: discarded_futures
stream: service.getAllUsers(),
builder: (context, snapshot) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
return CustomSemantics(
identifier: options.semantics.newChatGetUsersError,
value: "Error: ${snapshot.error}",
child: Text("Error: ${snapshot.error}"),
);
} else if (snapshot.hasData) {
return UserList(
users: snapshot.data!,

View file

@ -2,6 +2,7 @@ import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
import "package:flutter_chat/src/util/scope.dart";
@ -301,10 +302,15 @@ class _BodyState extends State<_Body> {
const SizedBox(
height: 16,
),
Text(
"${translations.selectedMembersHeader}"
"${users.length}",
style: theme.textTheme.titleMedium,
CustomSemantics(
identifier: options.semantics.newGroupChatMemberAmount,
value: "${translations.selectedMembersHeader}"
"${users.length}",
child: Text(
"${translations.selectedMembersHeader}"
"${users.length}",
style: theme.textTheme.titleMedium,
),
),
const SizedBox(
height: 12,

View file

@ -196,10 +196,17 @@ class _Body extends StatelessWidget {
// ignore: discarded_futures
stream: service.getAllUsers(),
builder: (context, snapshot) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
return Semantics(
identifier: options.semantics.newGroupChatGetUsersError,
value: "Error: ${snapshot.error}",
child: Text("Error: ${snapshot.error}"),
);
} else if (snapshot.hasData) {
return Stack(
children: [

View file

@ -1,5 +1,6 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_profile/flutter_profile.dart";
@ -106,9 +107,14 @@ class _UserListState extends State<UserList> {
const SizedBox(
width: 12,
),
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
CustomSemantics(
identifier: options.semantics
.newChatUserListUserFullName(index),
value: user.fullname ?? translations.anonymousUser,
child: Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
if (widget.creatingGroup) ...[
const Spacer(),
@ -154,9 +160,14 @@ class _UserListState extends State<UserList> {
const SizedBox(
width: 12,
),
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
CustomSemantics(
identifier: options.semantics
.newChatUserListUserFullName(index),
value: user.fullname ?? translations.anonymousUser,
child: Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
if (widget.creatingGroup) ...[
const Spacer(),