diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b9d2cfea19..78db445052 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -808,7 +808,7 @@ "type": "text", "placeholders": {} }, - "enableEncryption": "Enable encryption", + "enableEncryption": "Enable end-to-end encryption", "@enableEncryption": { "type": "text", "placeholders": {} @@ -2746,5 +2746,9 @@ "pasteImageFailed": "Paste image failed", "copyImageFailed": "Copy image failed", "fileFormatNotSupported": "File format not supported", - "copyImageSuccess": "Image copied to clipboard" + "copyImageSuccess": "Image copied to clipboard", + "encryptionMessage": "This feature protects your messages from being read by others, but also prevents them from being backed up on our servers. You can't disable this later.", + "encryptionWarning": "You might lose your messages if you access Twake app on the another device.", + "selectedUsers": "Selected users", + "clearAllSelected": "Clear all selected" } \ No newline at end of file diff --git a/lib/pages/new_group/contacts_selection_view.dart b/lib/pages/new_group/contacts_selection_view.dart index 44147f66aa..97e9f11649 100644 --- a/lib/pages/new_group/contacts_selection_view.dart +++ b/lib/pages/new_group/contacts_selection_view.dart @@ -37,9 +37,6 @@ class ContactsSelectionView extends StatelessWidget { ), body: Column( children: [ - SelectedParticipantsList( - contactsSelectionController: controller, - ), Expanded( child: ValueListenableBuilder( valueListenable: controller @@ -60,6 +57,11 @@ class ContactsSelectionView extends StatelessWidget { onRefresh: controller.fetchContacts, onLoading: controller.loadMoreContacts, slivers: [ + SliverToBoxAdapter( + child: SelectedParticipantsList( + contactsSelectionController: controller, + ), + ), ContactsSelectionList( contactsNotifier: controller.contactsNotifier!, selectedContactsMapNotifier: diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index def1574093..9c51a11657 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -54,6 +54,7 @@ class NewGroupController extends ContactsSelectionController final haveGroupNameNotifier = ValueNotifier(false); final groupNameFocusNode = FocusNode(); StreamSubscription? createNewGroupChatInteractorStreamSubscription; + final enableEncryptionNotifier = ValueNotifier(false); String groupName = ""; @@ -153,7 +154,7 @@ class NewGroupController extends ContactsSelectionController .map((contact) => contact.matrixId) .whereNotNull() .toList(), - enableEncryption: true, + enableEncryption: enableEncryptionNotifier.value, urlAvatar: urlAvatar, ), ); @@ -372,6 +373,10 @@ class NewGroupController extends ContactsSelectionController false; } + void toggleEnableEncryption() { + enableEncryptionNotifier.value = !enableEncryptionNotifier.value; + } + @override Widget build(BuildContext context) => ContactsSelectionView(this); } diff --git a/lib/pages/new_group/new_group_chat_info.dart b/lib/pages/new_group/new_group_chat_info.dart index 2f42acc98c..9bf0b33853 100644 --- a/lib/pages/new_group/new_group_chat_info.dart +++ b/lib/pages/new_group/new_group_chat_info.dart @@ -30,18 +30,13 @@ class NewGroupChatInfo extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(context), - body: Padding( - padding: NewGroupChatInfoStyle.padding, - child: LayoutBuilder( - builder: (context, constraint) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height - - MediaQuery.of(context).viewInsets.bottom, - ), - child: IntrinsicHeight( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverToBoxAdapter( child: Column( - mainAxisSize: MainAxisSize.min, children: [ Padding( padding: NewGroupChatInfoStyle.profilePadding, @@ -78,17 +73,32 @@ class NewGroupChatInfo extends StatelessWidget { const SizedBox(height: 32), _buildGroupNameTextField(context), const SizedBox(height: 16), - Expanded( - child: ExpansionParticipantsList( - newGroupController: newGroupController, - contactsList: contactsList, - ), + _EncryptionSettingTile( + enableEncryptionNotifier: + newGroupController.enableEncryptionNotifier, + onChanged: (value) { + newGroupController.toggleEnableEncryption(); + }, ), ], ), ), - ); - }, + ), + ]; + }, + body: Padding( + padding: NewGroupChatInfoStyle.padding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ExpansionParticipantsList( + newGroupController: newGroupController, + contactsList: contactsList, + ), + ), + ], + ), ), ), floatingActionButton: ValueListenableBuilder( @@ -320,3 +330,96 @@ class _AvatarForWebBuilder extends StatelessWidget { ); } } + +class _EncryptionSettingTile extends StatelessWidget { + final ValueNotifier enableEncryptionNotifier; + + final ValueChanged? onChanged; + + const _EncryptionSettingTile({ + required this.enableEncryptionNotifier, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: NewGroupChatInfoStyle.screenPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: NewGroupChatInfoStyle.topScreenPadding, + child: Icon( + Icons.lock, + ), + ), + const SizedBox( + width: 8.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: NewGroupChatInfoStyle.topScreenPadding, + child: Text( + L10n.of(context)!.enableEncryption, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const SizedBox(height: 4.0), + ValueListenableBuilder( + valueListenable: enableEncryptionNotifier, + builder: (context, isEnable, child) { + return Column( + children: [ + Text( + L10n.of(context)!.encryptionMessage, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: LinagoraRefColors.material().neutral[40], + ), + ), + AnimatedSize( + alignment: Alignment.topCenter, + duration: const Duration(milliseconds: 50), + child: isEnable + ? Text( + L10n.of(context)!.encryptionWarning, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: + Theme.of(context).colorScheme.error, + ), + ) + : const SizedBox.shrink(), + ), + ], + ); + }, + ), + ], + ), + ), + const SizedBox( + width: 12, + ), + ValueListenableBuilder( + valueListenable: enableEncryptionNotifier, + builder: (context, isEnable, child) { + return Checkbox( + value: isEnable, + onChanged: (value) => onChanged?.call(value), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/new_group/new_group_chat_info_style.dart b/lib/pages/new_group/new_group_chat_info_style.dart index c85676d2d8..967613e12a 100644 --- a/lib/pages/new_group/new_group_chat_info_style.dart +++ b/lib/pages/new_group/new_group_chat_info_style.dart @@ -34,4 +34,10 @@ class NewGroupChatInfoStyle { static const EdgeInsetsDirectional profilePadding = EdgeInsetsDirectional.only(top: 16.0); + + static const EdgeInsetsDirectional screenPadding = + EdgeInsetsDirectional.only(start: 8.0, bottom: 8.0, end: 4.0); + + static const EdgeInsetsDirectional topScreenPadding = + EdgeInsetsDirectional.only(top: 8.0); } diff --git a/lib/pages/new_group/selected_contacts_map_change_notifier.dart b/lib/pages/new_group/selected_contacts_map_change_notifier.dart index 0d15c47a6b..caac5232b4 100644 --- a/lib/pages/new_group/selected_contacts_map_change_notifier.dart +++ b/lib/pages/new_group/selected_contacts_map_change_notifier.dart @@ -2,17 +2,24 @@ import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:flutter/widgets.dart'; class SelectedContactsMapChangeNotifier extends ChangeNotifier { - final Map> selectedContactsMap = {}; + final selectedContactsMap = >{}; final haveSelectedContactsNotifier = ValueNotifier(false); + Set _selectedContactsList = {}; - Iterable get contactsList => selectedContactsMap.keys - .where((contact) => selectedContactsMap[contact]?.value ?? false); + Iterable get contactsList { + return _selectedContactsList; + } void onContactTileTap(BuildContext context, PresentationContact contact) { final oldVal = selectedContactsMap[contact]?.value ?? false; final newVal = !oldVal; selectedContactsMap.putIfAbsent(contact, () => ValueNotifier(newVal)); selectedContactsMap[contact]!.value = newVal; + if (newVal) { + _selectedContactsList.add(contact); + } else { + _selectedContactsList.remove(contact); + } notifyListeners(); haveSelectedContactsNotifier.value = contactsList.isNotEmpty; } @@ -26,6 +33,15 @@ class SelectedContactsMapChangeNotifier extends ChangeNotifier { if (selectedContactsMap.containsKey(contact)) { selectedContactsMap[contact]!.value = false; } + _selectedContactsList.remove(contact); + notifyListeners(); + } + + void unselectAllContacts() { + for (final contact in selectedContactsMap.keys) { + selectedContactsMap[contact]!.value = false; + } + _selectedContactsList = {}; notifyListeners(); } diff --git a/lib/pages/new_group/widget/selected_participants_list.dart b/lib/pages/new_group/widget/selected_participants_list.dart index ba74398ed9..ec2d3cdd25 100644 --- a/lib/pages/new_group/widget/selected_participants_list.dart +++ b/lib/pages/new_group/widget/selected_participants_list.dart @@ -1,6 +1,11 @@ import 'package:fluffychat/pages/new_group/contacts_selection.dart'; -import 'package:fluffychat/widgets/avatar/avatar_with_bottom_icon_widget.dart'; +import 'package:fluffychat/pages/new_group/widget/selected_participants_list_style.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class SelectedParticipantsList extends StatefulWidget { final ContactsSelectionController contactsSelectionController; @@ -20,61 +25,125 @@ class _SelectedParticipantsListState extends State { final contactsNotifier = widget.contactsSelectionController.selectedContactsMapNotifier; - return SizedBox( - width: MediaQuery.of(context).size.width, - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - // in 3.10.0, it has an ListenableBuilder for better in this case, temporary solution. - // no effect in performance, - child: AnimatedBuilder( - animation: contactsNotifier, - builder: (BuildContext context, Widget? child) { - Widget selectedContactsListWidget; - + return AnimatedSize( + curve: Curves.easeIn, + alignment: Alignment.bottomCenter, + duration: const Duration(milliseconds: 250), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: ListenableBuilder( + listenable: contactsNotifier, + builder: (context, Widget? child) { if (contactsNotifier.contactsList.isEmpty) { - selectedContactsListWidget = SizedBox( - width: MediaQuery.of(context).size.width, - ); - } else { - selectedContactsListWidget = Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: contactsNotifier.contactsList - .map( - (contact) => Tooltip( - message: '${contact.displayName}', - preferBelow: false, - child: InkWell( - borderRadius: BorderRadius.circular(12.0), - onTap: () => - contactsNotifier.unselectContact(contact), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: AvatarWithBottomIconWidget( - presentationContact: contact, - icon: Icons.close, + return const SizedBox.shrink(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SelectedParticipantsListStyle.paddingAll, + child: Wrap( + spacing: 8.0, + children: contactsNotifier.contactsList.map((contact) { + return InputChip( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SelectedParticipantsListStyle.borderRadiusChip, + ), + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + width: 1.0, + ), + ), + labelPadding: + SelectedParticipantsListStyle.labelChipPadding, + padding: const EdgeInsets.all(0), + label: Text( + contact.displayName ?? contact.matrixId ?? '', + style: + Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + avatar: contact.matrixId != null + ? FutureBuilder( + future: Matrix.of(context) + .client + .getProfileFromUserId( + contact.matrixId!, + getFromRooms: false, + ), + builder: ((context, snapshot) { + return Avatar( + mxContent: snapshot.data?.avatarUrl, + name: contact.displayName, + size: SelectedParticipantsListStyle + .avatarChipSize, + ); + }), + ) + : Avatar( + name: contact.displayName, + size: SelectedParticipantsListStyle + .avatarChipSize, + ), + onDeleted: () { + widget.contactsSelectionController + .selectedContactsMapNotifier + .unselectContact(contact); + }, + ); + }).toList(), + ), + ), + Divider( + color: Theme.of(context) + .colorScheme + .surfaceTint + .withOpacity(0.16), + ), + const SizedBox( + height: 4.0, + ), + Padding( + padding: SelectedParticipantsListStyle.contactPadding, + child: Row( + children: [ + Text( + '${L10n.of(context)!.selectedUsers}: ${contactsNotifier.contactsList.length}', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[20], + ), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () => widget.contactsSelectionController + .selectedContactsMapNotifier + .unselectAllContacts(), + child: Text( + L10n.of(context)!.clearAllSelected, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: LinagoraRefColors.material() + .tertiary[20], + ), ), ), - ), + ], ), - ) - .toList(), + ), + ], + ), ), - ); - } - - if (contactsNotifier.contactsList.length <= 1) { - return AnimatedSize( - curve: Curves.easeIn, - alignment: Alignment.bottomLeft, - duration: const Duration(milliseconds: 250), - child: selectedContactsListWidget, - ); - } - - return selectedContactsListWidget; + ], + ); }, ), ), diff --git a/lib/pages/new_group/widget/selected_participants_list_style.dart b/lib/pages/new_group/widget/selected_participants_list_style.dart new file mode 100644 index 0000000000..5e77888b72 --- /dev/null +++ b/lib/pages/new_group/widget/selected_participants_list_style.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class SelectedParticipantsListStyle { + static const EdgeInsetsDirectional paddingAll = + EdgeInsetsDirectional.only(top: 12.0, start: 16.0, end: 16.0); + + static const double borderRadiusChip = 16.0; + + static const EdgeInsetsDirectional labelChipPadding = + EdgeInsetsDirectional.only( + start: 8.0, + top: 2.0, + bottom: 2.0, + ); + + static const double avatarChipSize = 24.0; + + static const EdgeInsetsDirectional contactPadding = + EdgeInsetsDirectional.only( + start: 16.0, + end: 16.0, + bottom: 8.0, + ); +}