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, MessageModel? previousMessage,
UserModel? sender, UserModel? sender,
Function(UserModel sender) onPressSender, Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
); );
/// The group avatar builder /// 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:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_builders.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"; import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat options /// The chat options
@ -13,6 +14,7 @@ class ChatOptions {
this.groupChatEnabled = true, this.groupChatEnabled = true,
this.enableLoadingIndicator = true, this.enableLoadingIndicator = true,
this.translations = const ChatTranslations.empty(), this.translations = const ChatTranslations.empty(),
this.semantics = const ChatSemantics.standard(),
this.builders = const ChatBuilders(), this.builders = const ChatBuilders(),
this.spacing = const ChatSpacing(), this.spacing = const ChatSpacing(),
this.paginationControls = const ChatPaginationControls(), this.paginationControls = const ChatPaginationControls(),
@ -43,6 +45,9 @@ class ChatOptions {
/// [translations] is the chat translations. /// [translations] is the chat translations.
final ChatTranslations translations; final ChatTranslations translations;
/// [semantics] is the chat semantics.
final ChatSemantics semantics;
/// [builders] is the chat builders. /// [builders] is the chat builders.
final ChatBuilders 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 "dart:async";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/flutter_chat.dart"; import "package:flutter_chat/flutter_chat.dart";
/// A widget representing an entry point for a chat UI. /// A widget representing an entry point for a chat UI.
@ -16,6 +17,7 @@ class FlutterChatEntryWidget extends StatefulWidget {
this.iconColor = Colors.black, this.iconColor = Colors.black,
this.counterBackgroundColor = Colors.red, this.counterBackgroundColor = Colors.red,
this.textStyle, this.textStyle,
this.semanticIdUnreadMessages = "text_unread_messages_count",
super.key, super.key,
}); });
@ -46,6 +48,9 @@ class FlutterChatEntryWidget extends StatefulWidget {
/// The chat options /// The chat options
final ChatOptions? options; final ChatOptions? options;
/// Semantic Id for the unread messages text
final String semanticIdUnreadMessages;
@override @override
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState(); State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
} }
@ -121,9 +126,13 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
color: widget.counterBackgroundColor, color: widget.counterBackgroundColor,
), ),
child: Center( child: Center(
child: Text( child: CustomSemantics(
snapshot.data?.toString() ?? "0", identifier: widget.semanticIdUnreadMessages,
style: widget.textStyle, 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:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_options.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/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_bottom.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.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, highlightColor: Colors.transparent,
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
onTap: onPressChatTitle, onTap: onPressChatTitle,
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ?? child: CustomSemantics(
Text( identifier: options.semantics.chatChatTitle,
chatTitle ?? "", value: chatTitle ?? "",
overflow: TextOverflow.ellipsis, child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
), Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
), ),
); );
} }
@ -458,6 +463,9 @@ class _ChatBody extends HookWidget {
previousMessage: prevMsg, previousMessage: prevMsg,
sender: userMap[msg.senderId], sender: userMap[msg.senderId],
onPressSender: onPressUserProfile, 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:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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/screens/chat_detail/widgets/default_message_builder.dart";
import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
@ -18,15 +19,22 @@ class ChatNoMessages extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
var translations = chatScope.options.translations; var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context); var theme = Theme.of(context);
return Center( return Center(
child: Text( child: CustomSemantics(
isGroupChat identifier: options.semantics.chatNoMessages,
value: isGroupChat
? translations.writeFirstMessageInGroupChat ? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat, : 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.message,
required this.sender, required this.sender,
required this.onPressSender, required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
this.previousMessage, this.previousMessage,
super.key, super.key,
}); });
@ -56,6 +67,15 @@ class ChatBubble extends HookWidget {
/// Callback function when a message sender is pressed. /// Callback function when a message sender is pressed.
final Function(UserModel user) onPressSender; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var chatScope = ChatScope.of(context); var chatScope = ChatScope.of(context);
@ -67,12 +87,18 @@ class ChatBubble extends HookWidget {
previousMessage, previousMessage,
sender, sender,
onPressSender, onPressSender,
semanticIdTitle,
semanticIdTime,
semanticIdText,
) ?? ) ??
DefaultChatMessageBuilder( DefaultChatMessageBuilder(
message: message, message: message,
previousMessage: previousMessage, previousMessage: previousMessage,
sender: sender, sender: sender,
onPressSender: onPressSender, 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:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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/config/chat_options.dart";
import "package:flutter_chat/src/services/date_formatter.dart"; import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_chat/src/util/scope.dart"; import "package:flutter_chat/src/util/scope.dart";
@ -14,6 +15,9 @@ class DefaultChatMessageBuilder extends StatelessWidget {
required this.previousMessage, required this.previousMessage,
required this.sender, required this.sender,
required this.onPressSender, required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
super.key, super.key,
}); });
@ -30,6 +34,15 @@ class DefaultChatMessageBuilder extends StatelessWidget {
/// The function that is called when the sender is clicked /// The function that is called when the sender is clicked
final Function(UserModel user) onPressSender; 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] /// implements [ChatMessageBuilder]
static Widget builder( static Widget builder(
BuildContext context, BuildContext context,
@ -37,12 +50,18 @@ class DefaultChatMessageBuilder extends StatelessWidget {
MessageModel? previousMessage, MessageModel? previousMessage,
UserModel? sender, UserModel? sender,
Function(UserModel sender) onPressSender, Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
) => ) =>
DefaultChatMessageBuilder( DefaultChatMessageBuilder(
message: message, message: message,
previousMessage: previousMessage, previousMessage: previousMessage,
sender: sender, sender: sender,
onPressSender: onPressSender, onPressSender: onPressSender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
); );
/// Merges the [MessageTheme] from the themeresolver with the [MessageTheme] /// Merges the [MessageTheme] from the themeresolver with the [MessageTheme]
@ -87,6 +106,9 @@ class DefaultChatMessageBuilder extends StatelessWidget {
message: message, message: message,
messageTheme: messageTheme, messageTheme: messageTheme,
sender: sender, sender: sender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
); );
var messagePadding = messageTheme.messageSidePadding!; var messagePadding = messageTheme.messageSidePadding!;
@ -126,6 +148,9 @@ class _ChatMessageBubble extends StatelessWidget {
required this.previousMessage, required this.previousMessage,
required this.messageTheme, required this.messageTheme,
required this.sender, required this.sender,
required this.semanticIdTitle,
required this.semanticIdTime,
required this.semanticIdText,
}); });
final bool isSameSender; final bool isSameSender;
@ -134,6 +159,9 @@ class _ChatMessageBubble extends StatelessWidget {
final MessageModel? previousMessage; final MessageModel? previousMessage;
final MessageTheme messageTheme; final MessageTheme messageTheme;
final UserModel? sender; final UserModel? sender;
final String semanticIdTitle;
final String semanticIdTime;
final String semanticIdText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -154,9 +182,13 @@ class _ChatMessageBubble extends StatelessWidget {
var senderTitle = var senderTitle =
options.senderTitleResolver?.call(sender) ?? sender?.firstName ?? ""; options.senderTitleResolver?.call(sender) ?? sender?.firstName ?? "";
var senderTitleText = Text( var senderTitleText = CustomSemantics(
senderTitle, identifier: semanticIdTitle,
style: theme.textTheme.titleMedium, value: senderTitle,
child: Text(
senderTitle,
style: theme.textTheme.titleMedium,
),
); );
var messageTimeRow = Row( var messageTimeRow = Row(
@ -164,12 +196,16 @@ class _ChatMessageBubble extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 8, bottom: 4), padding: const EdgeInsets.only(right: 8, bottom: 4),
child: Text( child: CustomSemantics(
messageTime, identifier: semanticIdTime,
style: textTheme.bodySmall?.copyWith( value: messageTime,
color: messageTheme.textColor, 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, right: 12,
bottom: 4, bottom: 4,
), ),
child: Text( child: Semantics(
message.text!, identifier: semanticIdText,
style: textTheme.bodyLarge?.copyWith( value: message.text,
color: messageTheme.textColor, 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)), options.imageProviderResolver(context, Uri.parse(imageUrl)),
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) => Text( errorBuilder: (context, error, stackTrace) => Text(
// TODO: Non-replaceable text
"Something went wrong with loading the image", "Something went wrong with loading the image",
style: textTheme.bodyLarge?.copyWith( style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor, color: messageTheme.textColor,

View file

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

View file

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

View file

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

View file

@ -196,10 +196,17 @@ class _Body extends StatelessWidget {
// ignore: discarded_futures // ignore: discarded_futures
stream: service.getAllUsers(), stream: service.getAllUsers(),
builder: (context, snapshot) { builder: (context, snapshot) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) { } 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) { } else if (snapshot.hasData) {
return Stack( return Stack(
children: [ children: [

View file

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