feat: add semantics for buttons

This commit is contained in:
Jacques 2025-02-27 17:25:20 +01:00 committed by FlutterJoey
parent b3b8b1828e
commit 371ff6c335
15 changed files with 579 additions and 366 deletions

View file

@ -24,6 +24,7 @@
- Added enabled boolean to the messageInputBuilder and made parameters named
- Added autoScrollTriggerOffset to the ChatPaginationControls to adjust when the auto scroll should be enabled
- Added the ability to set the color of the CircularProgressIndicator of the ImageLoadingSnackbar by theme.snackBarTheme.actionTextColor
- Added semantics for variable text, buttons and textfields
## 4.0.0
- Move to the new user story architecture

View file

@ -33,6 +33,25 @@ class ChatSemantics {
required this.newChatBioInput,
required this.newChatSearchInput,
required this.newGroupChatSearchInput,
required this.profileStartChatButton,
required this.chatsStartChatButton,
required this.chatsDeleteConfirmButton,
required this.newChatCreateGroupChatButton,
required this.newGroupChatCreateGroupChatButton,
required this.newGroupChatNextButton,
required this.imagePickerCancelButton,
required this.chatSelectImageIconButton,
required this.chatSendMessageIconButton,
required this.newChatSearchIconButton,
required this.newGroupChatSearchIconButton,
required this.chatBackButton,
required this.chatTitleButton,
required this.newGroupChatSelectImage,
required this.newGroupChatRemoveImage,
required this.newGroupChatRemoveUser,
required this.profileTapUserButton,
required this.chatsOpenChatButton,
required this.userListTapUser,
});
/// Default translations for the chat component view
@ -58,6 +77,25 @@ class ChatSemantics {
this.newChatBioInput = "input_text_bio",
this.newChatSearchInput = "input_text_search",
this.newGroupChatSearchInput = "input_text_search",
this.profileStartChatButton = "button_start_chat",
this.chatsStartChatButton = "button_start_chat",
this.chatsDeleteConfirmButton = "button_delete_chat_confirm",
this.newChatCreateGroupChatButton = "button_create_group_chat",
this.newGroupChatCreateGroupChatButton = "button_create_group_chat",
this.newGroupChatNextButton = "button_next",
this.imagePickerCancelButton = "button_cancel",
this.chatSelectImageIconButton = "button_icon_select_image",
this.chatSendMessageIconButton = "button_icon_send_message",
this.newChatSearchIconButton = "button_icon_search",
this.newGroupChatSearchIconButton = "button_icon_search",
this.chatBackButton = "button_back",
this.chatTitleButton = "button_open_profile",
this.newGroupChatSelectImage = "button_select_image",
this.newGroupChatRemoveImage = "button_remove_image",
this.newGroupChatRemoveUser = "button_remove_user",
this.profileTapUserButton = _defaultProfileTapUserButton,
this.chatsOpenChatButton = _defaultChatsOpenChatButton,
this.userListTapUser = _defaultUserListTapUser,
});
// Text
@ -87,6 +125,33 @@ class ChatSemantics {
final String newChatSearchInput;
final String newGroupChatSearchInput;
// Buttons
final String profileStartChatButton;
final String chatsStartChatButton;
final String chatsDeleteConfirmButton;
final String newChatCreateGroupChatButton;
final String newGroupChatCreateGroupChatButton;
final String newGroupChatNextButton;
final String imagePickerCancelButton;
// Icon buttons
final String chatSelectImageIconButton;
final String chatSendMessageIconButton;
final String newChatSearchIconButton;
final String newGroupChatSearchIconButton;
// Inkwells
final String chatBackButton;
final String chatTitleButton;
final String newGroupChatSelectImage;
final String newGroupChatRemoveImage;
final String newGroupChatRemoveUser;
// Indexed inkwells
final String Function(int index) profileTapUserButton;
final String Function(int index) chatsOpenChatButton;
final String Function(int index) userListTapUser;
ChatSemantics copyWith({
String? profileTitle,
String? profileDescription,
@ -109,6 +174,25 @@ class ChatSemantics {
String? newChatBioInput,
String? newChatSearchInput,
String? newGroupChatSearchInput,
String? profileStartChatButton,
String? chatsStartChatButton,
String? chatsDeleteConfirmButton,
String? newChatCreateGroupChatButton,
String? newGroupChatCreateGroupChatButton,
String? newGroupChatNextButton,
String? imagePickerCancelButton,
String? chatSelectImageIconButton,
String? chatSendMessageIconButton,
String? newChatSearchIconButton,
String? newGroupChatSearchIconButton,
String? chatBackButton,
String? chatTitleButton,
String? newGroupChatSelectImage,
String? newGroupChatRemoveImage,
String? newGroupChatRemoveUser,
String Function(int)? profileTapUserButton,
String Function(int)? chatsOpenChatButton,
String Function(int)? userListTapUser,
}) =>
ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle,
@ -137,6 +221,38 @@ class ChatSemantics {
newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput,
newGroupChatSearchInput:
newGroupChatSearchInput ?? this.newGroupChatSearchInput,
profileStartChatButton:
profileStartChatButton ?? this.profileStartChatButton,
chatsStartChatButton: chatsStartChatButton ?? this.chatsStartChatButton,
chatsDeleteConfirmButton:
chatsDeleteConfirmButton ?? this.chatsDeleteConfirmButton,
newChatCreateGroupChatButton:
newChatCreateGroupChatButton ?? this.newChatCreateGroupChatButton,
newGroupChatCreateGroupChatButton: newGroupChatCreateGroupChatButton ??
this.newGroupChatCreateGroupChatButton,
newGroupChatNextButton:
newGroupChatNextButton ?? this.newGroupChatNextButton,
imagePickerCancelButton:
imagePickerCancelButton ?? this.imagePickerCancelButton,
chatSelectImageIconButton:
chatSelectImageIconButton ?? this.chatSelectImageIconButton,
chatSendMessageIconButton:
chatSendMessageIconButton ?? this.chatSendMessageIconButton,
newChatSearchIconButton:
newChatSearchIconButton ?? this.newChatSearchIconButton,
newGroupChatSearchIconButton:
newGroupChatSearchIconButton ?? this.newGroupChatSearchIconButton,
chatBackButton: chatBackButton ?? this.chatBackButton,
chatTitleButton: chatTitleButton ?? this.chatTitleButton,
newGroupChatSelectImage:
newGroupChatSelectImage ?? this.newGroupChatSelectImage,
newGroupChatRemoveImage:
newGroupChatRemoveImage ?? this.newGroupChatRemoveImage,
newGroupChatRemoveUser:
newGroupChatRemoveUser ?? this.newGroupChatRemoveUser,
profileTapUserButton: profileTapUserButton ?? this.profileTapUserButton,
chatsOpenChatButton: chatsOpenChatButton ?? this.chatsOpenChatButton,
userListTapUser: userListTapUser ?? this.userListTapUser,
);
}
@ -150,3 +266,6 @@ 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";
String _defaultProfileTapUserButton(int index) => "button_tap_user_$index";
String _defaultChatsOpenChatButton(int index) => "button_open_chat_$index";
String _defaultUserListTapUser(int index) => "button_tap_user_$index";

View file

@ -18,6 +18,7 @@ class FlutterChatEntryWidget extends StatefulWidget {
this.counterBackgroundColor = Colors.red,
this.textStyle,
this.semanticIdUnreadMessages = "text_unread_messages_count",
this.semanticIdOpenButton = "button_open_chat",
super.key,
});
@ -51,6 +52,9 @@ class FlutterChatEntryWidget extends StatefulWidget {
/// Semantic Id for the unread messages text
final String semanticIdUnreadMessages;
/// Semantic Id for the unread messages text
final String semanticIdOpenButton;
@override
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
}
@ -83,61 +87,65 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
}
@override
Widget build(BuildContext context) => InkWell(
onTap: () async =>
widget.onTap?.call() ??
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlutterChatNavigatorUserstory(
userId: widget.userId,
options: widget.options ?? ChatOptions(),
),
),
),
child: StreamBuilder<int>(
stream: chatService.getUnreadMessagesCount(),
builder: (BuildContext context, snapshot) => Stack(
alignment: Alignment.center,
children: [
Container(
width: widget.widgetSize,
height: widget.widgetSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.backgroundColor,
),
child: _AnimatedNotificationIcon(
icon: Icon(
widget.icon,
color: widget.iconColor,
size: widget.widgetSize / 1.5,
Widget build(BuildContext context) => CustomSemantics(
identifier: widget.semanticIdOpenButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () async =>
widget.onTap?.call() ??
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlutterChatNavigatorUserstory(
userId: widget.userId,
options: widget.options ?? ChatOptions(),
),
notifications: snapshot.data ?? 0,
),
),
Positioned(
right: 0.0,
top: 0.0,
child: Container(
width: widget.widgetSize / 2,
height: widget.widgetSize / 2,
child: StreamBuilder<int>(
stream: chatService.getUnreadMessagesCount(),
builder: (BuildContext context, snapshot) => Stack(
alignment: Alignment.center,
children: [
Container(
width: widget.widgetSize,
height: widget.widgetSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.counterBackgroundColor,
color: widget.backgroundColor,
),
child: Center(
child: CustomSemantics(
identifier: widget.semanticIdUnreadMessages,
value: snapshot.data?.toString() ?? "0",
child: Text(
snapshot.data?.toString() ?? "0",
style: widget.textStyle,
child: _AnimatedNotificationIcon(
icon: Icon(
widget.icon,
color: widget.iconColor,
size: widget.widgetSize / 1.5,
),
notifications: snapshot.data ?? 0,
),
),
Positioned(
right: 0.0,
top: 0.0,
child: Container(
width: widget.widgetSize / 2,
height: widget.widgetSize / 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.counterBackgroundColor,
),
child: Center(
child: CustomSemantics(
identifier: widget.semanticIdUnreadMessages,
value: snapshot.data?.toString() ?? "0",
child: Text(
snapshot.data?.toString() ?? "0",
style: widget.textStyle,
),
),
),
),
),
),
],
],
),
),
),
);

View file

@ -3,8 +3,8 @@ 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/chat_options.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";
@ -199,9 +199,12 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
Widget? appBarIcon;
if (onPressBack != null) {
appBarIcon = InkWell(
onTap: onPressBack,
child: const Icon(Icons.arrow_back_ios),
appBarIcon = CustomSemantics(
identifier: options.semantics.chatBackButton,
child: InkWell(
onTap: onPressBack,
child: const Icon(Icons.arrow_back_ios),
),
);
}
@ -209,19 +212,23 @@ class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
iconTheme: theme.appBarTheme.iconTheme,
centerTitle: true,
leading: appBarIcon,
title: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: onPressChatTitle,
child: CustomSemantics(
identifier: options.semantics.chatChatTitle,
value: chatTitle ?? "",
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
title: CustomSemantics(
identifier: options.semantics.chatTitleButton,
buttonWithVariableText: true,
child: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: onPressChatTitle,
child: CustomSemantics(
identifier: options.semantics.chatChatTitle,
value: chatTitle ?? "",
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
),
),
);

View file

@ -69,20 +69,26 @@ class ChatBottomInputSection extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
alignment: Alignment.bottomRight,
onPressed: isLoading ? null : onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: options.iconEnabledColor,
CustomSemantics(
identifier: options.semantics.chatSelectImageIconButton,
child: IconButton(
alignment: Alignment.bottomRight,
onPressed: isLoading ? null : onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: options.iconEnabledColor,
),
),
),
IconButton(
alignment: Alignment.bottomRight,
disabledColor: options.iconDisabledColor,
color: options.iconEnabledColor,
onPressed: isLoading ? null : onClickSendMessage,
icon: const Icon(Icons.send_rounded),
CustomSemantics(
identifier: options.semantics.chatSendMessageIconButton,
child: IconButton(
alignment: Alignment.bottomRight,
disabledColor: options.iconDisabledColor,
color: options.iconEnabledColor,
onPressed: isLoading ? null : onClickSendMessage,
icon: const Icon(Icons.send_rounded),
),
),
],
),
@ -119,7 +125,8 @@ class ChatBottomInputSection extends HookWidget {
top: 16,
bottom: 16,
),
// this ensures that that there is space at the end of the textfield
// this ensures that that there is space at the end of the
// textfield
suffixIcon: AbsorbPointer(
child: Opacity(
opacity: 0.0,

View file

@ -297,7 +297,7 @@ class _DefaultChatImage extends StatelessWidget {
options.imageProviderResolver(context, Uri.parse(imageUrl)),
fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) => Text(
// TODO: Non-replaceable text
// TODO(Jacques): Non-replaceable text
"Something went wrong with loading the image",
style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor,

View file

@ -130,55 +130,63 @@ class _Body extends StatelessWidget {
var chatUserDisplay = Wrap(
children: [
if (chat != null) ...[
...chat!.users.map(
(tappedUser) => Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: InkWell(
onTap: () => onTapUser?.call(tappedUser),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
...chat!.users.asMap().entries.map(
(entry) {
var index = entry.key;
var tappedUser = entry.value;
var user = snapshot.data;
return Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: CustomSemantics(
identifier: options.semantics.profileTapUserButton(index),
child: InkWell(
onTap: () => onTapUser?.call(tappedUser),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (user == null) {
return const SizedBox.shrink();
}
var user = snapshot.data;
return options.builders.userAvatarBuilder?.call(
context,
user,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != null || user.imageUrl != ""
if (user == null) {
return const SizedBox.shrink();
}
return options.builders.userAvatarBuilder?.call(
context,
user,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != null ||
user.imageUrl != ""
? user.imageUrl
: null,
),
size: 60,
);
},
),
size: 60,
);
},
),
],
),
],
),
),
),
),
);
},
),
],
],
@ -286,18 +294,21 @@ class _Body extends StatelessWidget {
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: () {
onPressStartChat?.call(user!.id);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.newChatButton,
style: theme.textTheme.displayLarge,
),
],
child: CustomSemantics(
identifier: options.semantics.profileStartChatButton,
child: FilledButton(
onPressed: () {
onPressStartChat?.call(user!.id);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.newChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),

View file

@ -197,6 +197,8 @@ class _BodyState extends State<_Body> {
semantics.chatsChatLastUsed(index),
semanticIdUnreadMessages:
semantics.chatsChatUnreadMessages(index),
semanticIdButton:
semantics.chatsOpenChatButton(index),
);
return !chat.canBeDeleted
@ -275,18 +277,21 @@ class _BodyState extends State<_Body> {
vertical: 24,
horizontal: 4,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
child: CustomSemantics(
identifier: options.semantics.chatsStartChatButton,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
),
onPressed: widget.onPressStartChat,
child: Text(
translations.newChatButton,
style: theme.textTheme.displayLarge,
),
),
onPressed: widget.onPressStartChat,
child: Text(
translations.newChatButton,
style: theme.textTheme.displayLarge,
),
),
),
@ -303,6 +308,7 @@ class _ChatItem extends StatelessWidget {
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
required this.semanticIdButton,
});
final ChatModel chat;
@ -311,6 +317,7 @@ class _ChatItem extends StatelessWidget {
final String semanticIdSubTitle;
final String semanticIdLastUsed;
final String semanticIdUnreadMessages;
final String semanticIdButton;
@override
Widget build(BuildContext context) {
@ -330,29 +337,33 @@ class _ChatItem extends StatelessWidget {
semanticIdUnreadMessages: semanticIdUnreadMessages,
);
return InkWell(
onTap: () {
onPressChat(chat);
},
child: options.builders.chatRowContainerBuilder?.call(
context,
chatListItem,
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
return CustomSemantics(
identifier: semanticIdButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () {
onPressChat(chat);
},
child: options.builders.chatRowContainerBuilder?.call(
context,
chatListItem,
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: chatListItem,
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: chatListItem,
),
),
),
);
}
}
@ -530,6 +541,10 @@ Future<bool?> _deleteDialog(
) async {
var theme = Theme.of(context);
var scope = ChatScope.of(context);
var options = scope.options;
return showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
@ -555,20 +570,23 @@ Future<bool?> _deleteDialog(
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: FilledButton(
onPressed: () {
Navigator.of(
context,
).pop(true);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.deleteChatModalConfirm,
style: theme.textTheme.displayLarge,
),
],
child: CustomSemantics(
identifier: options.semantics.chatsDeleteConfirmButton,
child: FilledButton(
onPressed: () {
Navigator.of(
context,
).pop(true);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.deleteChatModalConfirm,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),

View file

@ -145,6 +145,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
semanticId: options.semantics.newChatSearchIconButton,
),
],
);
@ -187,23 +188,26 @@ class _Body extends StatelessWidget {
right: 32,
top: 20,
),
child: FilledButton(
onPressed: onPressCreateGroupChat,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.groups_2,
),
const SizedBox(
width: 8,
),
Text(
translations.newGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
child: CustomSemantics(
identifier: options.semantics.newChatCreateGroupChatButton,
child: FilledButton(
onPressed: onPressCreateGroupChat,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.groups_2,
),
const SizedBox(
width: 8,
),
Text(
translations.newGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),

View file

@ -158,31 +158,35 @@ class _BodyState extends State<_Body> {
Center(
child: Stack(
children: [
InkWell(
onTap: () async => onPressSelectImage(
context,
options,
(image) {
setState(() {
this.image = image;
});
},
),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(40),
image: image != null
? DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.cover,
)
CustomSemantics(
identifier: options.semantics.newGroupChatSelectImage,
child: InkWell(
onTap: () async => onPressSelectImage(
context,
options,
(image) {
setState(() {
this.image = image;
});
},
),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(40),
image: image != null
? DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.cover,
)
: null,
),
child: image == null
? const Icon(Icons.image)
: null,
),
child:
image == null ? const Icon(Icons.image) : null,
),
),
if (image != null)
@ -197,15 +201,19 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: InkWell(
onTap: () {
setState(() {
image = null;
});
},
child: const Icon(
Icons.close,
size: 12,
child: CustomSemantics(
identifier:
options.semantics.newGroupChatRemoveImage,
child: InkWell(
onTap: () {
setState(() {
image = null;
});
},
child: const Icon(
Icons.close,
size: 12,
),
),
),
),
@ -354,31 +362,34 @@ class _BodyState extends State<_Body> {
),
child: ValueListenableBuilder(
valueListenable: isButtonEnabled,
builder: (context, isEnabled, child) => FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) {
isPressed = true;
if (formKey.currentState!.validate()) {
await widget.onComplete(
users,
_chatNameController.text,
_bioController.text,
image,
);
builder: (context, isEnabled, child) => CustomSemantics(
identifier: "",
child: FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) {
isPressed = true;
if (formKey.currentState!.validate()) {
await widget.onComplete(
users,
_chatNameController.text,
_bioController.text,
image,
);
}
isPressed = false;
}
isPressed = false;
}
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.createGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.createGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
@ -402,38 +413,41 @@ class _SelectedUser extends StatelessWidget {
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
return InkWell(
onTap: () {
onRemove(user);
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
return CustomSemantics(
identifier: options.semantics.newGroupChatRemoveUser,
child: InkWell(
onTap: () {
onRemove(user);
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
),
size: 40,
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
),
),
],
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
),
),
],
),
),
);
}

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";
@ -155,6 +156,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
semanticId: options.semantics.newGroupChatSearchIconButton,
),
],
);
@ -272,18 +274,21 @@ class _NextButton extends StatelessWidget {
),
child: Visibility(
visible: selectedUsers.isNotEmpty,
child: FilledButton(
onPressed: () async {
await onPressGroupChatOverview(selectedUsers);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.next,
style: theme.textTheme.displayLarge,
),
],
child: CustomSemantics(
identifier: options.semantics.newGroupChatNextButton,
child: FilledButton(
onPressed: () async {
await onPressGroupChatOverview(selectedUsers);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.next,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),

View file

@ -1,6 +1,7 @@
import "dart:typed_data";
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_translations.dart";
import "package:flutter_chat/src/util/scope.dart";
@ -71,13 +72,16 @@ class DefaultImagePickerDialog extends StatelessWidget {
Icons.insert_drive_file_rounded,
size: 60,
),
closeButtonBuilder: (ontap) => TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
translations.cancelImagePickerBtn,
style: textTheme.bodyMedium!.copyWith(
fontSize: 18,
decoration: TextDecoration.underline,
closeButtonBuilder: (ontap) => CustomSemantics(
identifier: options.semantics.imagePickerCancelButton,
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
translations.cancelImagePickerBtn,
style: textTheme.bodyMedium!.copyWith(
fontSize: 18,
decoration: TextDecoration.underline,
),
),
),
),

View file

@ -6,6 +6,7 @@ class SearchIcon extends StatelessWidget {
const SearchIcon({
required this.isSearching,
required this.onPressed,
required this.semanticId,
super.key,
});
@ -15,14 +16,20 @@ class SearchIcon extends StatelessWidget {
/// Callback function triggered when the search icon is pressed
final VoidCallback onPressed;
/// Semantic id for icon button
final String semanticId;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return IconButton(
onPressed: onPressed,
icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
return Semantics(
identifier: semanticId,
child: IconButton(
onPressed: onPressed,
icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
),
),
);
}

View file

@ -80,70 +80,20 @@ class _UserListState extends State<UserList> {
itemBuilder: (context, index) {
var user = filteredUsers[index];
var isSelected = widget.selectedUsers.any((u) => u.id == user.id);
return InkWell(
onTap: () async {
if (widget.creatingGroup) {
return handleGroupChatTap(user);
} else {
return handlePersonalChatTap(user);
}
},
child: options.builders.chatRowContainerBuilder?.call(
context,
Row(
children: [
options.builders.userAvatarBuilder
?.call(context, user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 44,
),
const SizedBox(
width: 12,
),
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(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
return CustomSemantics(
identifier: options.semantics.userListTapUser(index),
buttonWithVariableText: true,
child: InkWell(
onTap: () async {
if (widget.creatingGroup) {
return handleGroupChatTap(user);
} else {
return handlePersonalChatTap(user);
}
},
child: options.builders.chatRowContainerBuilder?.call(
context,
Row(
children: [
options.builders.userAvatarBuilder
?.call(context, user, 44) ??
@ -183,8 +133,63 @@ class _UserListState extends State<UserList> {
],
],
),
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
options.builders.userAvatarBuilder
?.call(context, user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
),
size: 44,
),
const SizedBox(
width: 12,
),
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(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
),
),
),
),
);
},
),

View file

@ -15,6 +15,9 @@ dependencies:
intl: any
flutter_hooks: ^0.20.5
flutter_accessibility:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^0.0.2
flutter_image_picker:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^4.0.0