diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 5a65a2d6..a428e088 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -896,7 +896,7 @@ "type": "text", "placeholders": {} }, - "enterAnEmailAddress": "E-Mail-Adresse (nicht erforderlich)", + "enterAnEmailAddress": "Gib eine E-Mail-Adresse ein", "@enterAnEmailAddress": { "type": "text", "placeholders": {} diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 3aa8566e..b0741688 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -955,13 +955,11 @@ "type": "text", "placeholders": {} }, - "enterAnEmailAddress": "Email address (not mandatory)", + "enterAnEmailAddress": "Enter an email address", "@enterAnEmailAddress": { "type": "text", "placeholders": {} }, - "emailContinueAnyway": "Continue anyway", - "emailEmptyWarning": "Warning: Even though adding a mail address leaks sensitive personal data, you have no way to recover your password without it.", "enterASpacepName": "Enter a space name", "@enterASpacepName": {}, "enterAUsername": "Enter a username", diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index d3fb92f1..f9b8fbb7 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -910,7 +910,7 @@ "type": "text", "placeholders": {} }, - "enterAnEmailAddress": "Adresse de courriel (ne pas mandatoire)", + "enterAnEmailAddress": "Saisissez une adresse de courriel", "@enterAnEmailAddress": { "type": "text", "placeholders": {} diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 47096ce3..bbe06ce2 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -82,7 +82,10 @@ class ChatListController extends State with TickerProviderStateMixin { } void setActiveSpacesEntry(BuildContext context, SpacesEntry? spaceId) { - if (snappingSheetController.currentPosition != kSpacesBottomBarHeight) { + if ((snappingSheetController.isAttached + ? snappingSheetController.currentPosition + : 0) != + kSpacesBottomBarHeight) { snapBackSpacesSheet(); } setState(() => _activeSpacesEntry = spaceId); diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 438dd281..36300d17 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart'; +import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/pages/chat_list/stories_header.dart'; import '../../utils/stream_extension.dart'; import '../../widgets/matrix.dart'; @@ -28,7 +29,7 @@ class _ChatListViewBodyState extends State { // used to check the animation direction String? _lastUserId; - String? _lastSpaceId; + SpacesEntry? _lastSpace; @override void initState() { @@ -175,11 +176,13 @@ class _ChatListViewBodyState extends State { return SharedAxisTransition( animation: primaryAnimation, secondaryAnimation: secondaryAnimation, - transitionType: - widget.controller.snappingSheetController.currentPosition == - kSpacesBottomBarHeight - ? SharedAxisTransitionType.horizontal - : SharedAxisTransitionType.vertical, + transitionType: (widget.controller.snappingSheetController.isAttached + ? widget + .controller.snappingSheetController.currentPosition + : 0) == + kSpacesBottomBarHeight + ? SharedAxisTransitionType.horizontal + : SharedAxisTransitionType.vertical, fillColor: Theme.of(context).scaffoldBackgroundColor, child: child, ); @@ -208,13 +211,13 @@ class _ChatListViewBodyState extends State { } // otherwise, the space changed... else { - reversed = widget.controller.spaces - .indexWhere((element) => element.id == _lastSpaceId) < - widget.controller.spaces.indexWhere( - (element) => element.id == widget.controller.activeSpaceId); + reversed = widget.controller.spacesEntries + .indexWhere((element) => element == _lastSpace) < + widget.controller.spacesEntries.indexWhere( + (element) => element == widget.controller.activeSpacesEntry); } _lastUserId = newClient.userID; - _lastSpaceId = widget.controller.activeSpaceId; + _lastSpace = widget.controller.activeSpacesEntry; return reversed; } } diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart new file mode 100644 index 00000000..d6b27878 --- /dev/null +++ b/lib/pages/chat_list/chat_list_header.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:animations/animations.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +import 'package:vrouter/vrouter.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; +import '../../widgets/matrix.dart'; + +class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { + final ChatListController controller; + + const ChatListHeader({Key? key, required this.controller}) : super(key: key); + + @override + Widget build(BuildContext context) { + final selectMode = controller.selectMode; + + return AppBar( + elevation: controller.scrolledToTop ? 0 : null, + actionsIconTheme: IconThemeData( + color: controller.selectedRoomIds.isEmpty + ? null + : Theme.of(context).colorScheme.primary, + ), + leading: Matrix.of(context).isMultiAccount + ? ClientChooserButton(controller) + : selectMode == SelectMode.normal + ? null + : IconButton( + tooltip: L10n.of(context)!.cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelAction, + color: Theme.of(context).colorScheme.primary, + ), + centerTitle: false, + actions: selectMode == SelectMode.share + ? null + : selectMode == SelectMode.select + ? [ + if (controller.spaces.isNotEmpty) + IconButton( + tooltip: L10n.of(context)!.addToSpace, + icon: const Icon(Icons.group_work_outlined), + onPressed: controller.addOrRemoveToSpace, + ), + IconButton( + tooltip: L10n.of(context)!.toggleUnread, + icon: Icon(controller.anySelectedRoomNotMarkedUnread + ? Icons.mark_chat_read_outlined + : Icons.mark_chat_unread_outlined), + onPressed: controller.toggleUnread, + ), + IconButton( + tooltip: L10n.of(context)!.toggleFavorite, + icon: Icon(controller.anySelectedRoomNotFavorite + ? Icons.push_pin_outlined + : Icons.push_pin), + onPressed: controller.toggleFavouriteRoom, + ), + IconButton( + icon: Icon(controller.anySelectedRoomNotMuted + ? Icons.notifications_off_outlined + : Icons.notifications_outlined), + tooltip: L10n.of(context)!.toggleMuted, + onPressed: controller.toggleMuted, + ), + IconButton( + icon: const Icon(Icons.delete_outlined), + tooltip: L10n.of(context)!.archive, + onPressed: controller.archiveAction, + ), + ] + : [ + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.keyF + }, + onKeysPressed: () => VRouter.of(context).to('/search'), + helpLabel: L10n.of(context)!.search, + child: IconButton( + icon: const Icon(Icons.search_outlined), + tooltip: L10n.of(context)!.search, + onPressed: () => VRouter.of(context).to('/search'), + ), + ), + if (selectMode == SelectMode.normal) + IconButton( + icon: const Icon(Icons.camera_alt_outlined), + tooltip: L10n.of(context)!.addToStory, + onPressed: () => + VRouter.of(context).to('/stories/create'), + ), + PopupMenuButton( + onSelected: controller.onPopupMenuSelect, + itemBuilder: (_) => [ + PopupMenuItem( + value: PopupMenuAction.setStatus, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.edit_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.setStatus), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.newGroup, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_add_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.createNewGroup), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.newSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_work_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.createNewSpace), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.share_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.inviteContact), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.archive, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.archive_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.archive), + ], + ), + ), + PopupMenuItem( + value: PopupMenuAction.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.settings_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.settings), + ], + ), + ), + ], + ), + ], + title: PageTransitionSwitcher( + reverse: false, + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + layoutBuilder: (children) => Stack( + alignment: AlignmentDirectional.centerStart, + children: children, + ), + child: selectMode == SelectMode.share + ? Text( + L10n.of(context)!.share, + key: const ValueKey(SelectMode.share), + ) + : selectMode == SelectMode.select + ? Text( + controller.selectedRoomIds.length.toString(), + key: const ValueKey(SelectMode.select), + ) + : (() { + final name = controller.activeSpaceId == null + ? AppConfig.applicationName + : Matrix.of(context) + .client + .getRoomById(controller.activeSpaceId!)! + .displayname; + return Text(name, key: ValueKey(name)); + })(), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(56); +} diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index b6d6d381..02bffdec 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -7,13 +7,12 @@ import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:snapping_sheet/snapping_sheet.dart'; import 'package:vrouter/vrouter.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; import 'chat_list_body.dart'; +import 'chat_list_header.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; @@ -35,167 +34,7 @@ class ChatListView extends StatelessWidget { if (selMode == SelectMode.select) redirector.stopRedirection(); }, child: Scaffold( - appBar: AppBar( - elevation: controller.scrolledToTop ? 0 : null, - actionsIconTheme: IconThemeData( - color: controller.selectedRoomIds.isEmpty - ? null - : Theme.of(context).colorScheme.primary, - ), - leading: Matrix.of(context).isMultiAccount - ? ClientChooserButton(controller) - : selectMode == SelectMode.normal - ? null - : IconButton( - tooltip: L10n.of(context)!.cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelAction, - color: Theme.of(context).colorScheme.primary, - ), - centerTitle: false, - actions: selectMode == SelectMode.share - ? null - : selectMode == SelectMode.select - ? [ - if (controller.spaces.isNotEmpty) - IconButton( - tooltip: L10n.of(context)!.addToSpace, - icon: const Icon(Icons.group_work_outlined), - onPressed: controller.addOrRemoveToSpace, - ), - IconButton( - tooltip: L10n.of(context)!.toggleUnread, - icon: Icon(controller.anySelectedRoomNotMarkedUnread - ? Icons.mark_chat_read_outlined - : Icons.mark_chat_unread_outlined), - onPressed: controller.toggleUnread, - ), - IconButton( - tooltip: L10n.of(context)!.toggleFavorite, - icon: Icon(controller.anySelectedRoomNotFavorite - ? Icons.push_pin_outlined - : Icons.push_pin), - onPressed: controller.toggleFavouriteRoom, - ), - IconButton( - icon: Icon(controller.anySelectedRoomNotMuted - ? Icons.notifications_off_outlined - : Icons.notifications_outlined), - tooltip: L10n.of(context)!.toggleMuted, - onPressed: controller.toggleMuted, - ), - IconButton( - icon: const Icon(Icons.delete_outlined), - tooltip: L10n.of(context)!.archive, - onPressed: controller.archiveAction, - ), - ] - : [ - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyF - }, - onKeysPressed: () => - VRouter.of(context).to('/search'), - helpLabel: L10n.of(context)!.search, - child: IconButton( - icon: const Icon(Icons.search_outlined), - tooltip: L10n.of(context)!.search, - onPressed: () => - VRouter.of(context).to('/search'), - ), - ), - if (selectMode == SelectMode.normal) - IconButton( - icon: const Icon(Icons.camera_alt_outlined), - tooltip: L10n.of(context)!.addToStory, - onPressed: () => - VRouter.of(context).to('/stories/create'), - ), - PopupMenuButton( - onSelected: controller.onPopupMenuSelect, - itemBuilder: (_) => [ - PopupMenuItem( - value: PopupMenuAction.setStatus, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.edit_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.setStatus), - ], - ), - ), - PopupMenuItem( - value: PopupMenuAction.newGroup, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.group_add_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.createNewGroup), - ], - ), - ), - PopupMenuItem( - value: PopupMenuAction.newSpace, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.group_work_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.createNewSpace), - ], - ), - ), - PopupMenuItem( - value: PopupMenuAction.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.share_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.inviteContact), - ], - ), - ), - PopupMenuItem( - value: PopupMenuAction.archive, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.archive_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.archive), - ], - ), - ), - PopupMenuItem( - value: PopupMenuAction.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.settings), - ], - ), - ), - ], - ), - ], - title: Text(selectMode == SelectMode.share - ? L10n.of(context)!.share - : selectMode == SelectMode.select - ? controller.selectedRoomIds.length.toString() - : controller.activeSpaceId == null - ? AppConfig.applicationName - : Matrix.of(context) - .client - .getRoomById(controller.activeSpaceId!)! - .displayname), - ), + appBar: ChatListHeader(controller: controller), body: LayoutBuilder( builder: (context, size) { controller.snappingSheetContainerSize = size; diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 86a512f8..cdd844a6 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -123,12 +123,15 @@ class ClientChooserButton extends StatelessWidget { onKeysPressed: () => _previousAccount(matrix), ), PopupMenuButton( - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - matrix.client.userID!.localpart, - size: 28, - fontSize: 12, + child: Material( + borderRadius: BorderRadius.zero, + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + matrix.client.userID!.localpart, + size: 28, + fontSize: 12, + ), ), onSelected: _clientSelected, itemBuilder: _bundleMenuItems, diff --git a/lib/pages/chat_list/spaces_bottom_bar.dart b/lib/pages/chat_list/spaces_bottom_bar.dart index d5b812c2..2b032652 100644 --- a/lib/pages/chat_list/spaces_bottom_bar.dart +++ b/lib/pages/chat_list/spaces_bottom_bar.dart @@ -12,6 +12,8 @@ import 'package:fluffychat/widgets/matrix.dart'; const kSpacesBottomBarHeight = 56.0; +final GlobalKey _globalKey = GlobalKey(); + class SpacesBottomBar extends StatelessWidget { final ChatListController controller; @@ -37,13 +39,16 @@ class SpacesBottomBar extends StatelessWidget { return SingleChildScrollView( controller: controller.snappingSheetScrollContentController, child: AnimatedBuilder( - child: _SpacesBottomNavigation(controller: controller), + child: _SpacesBottomNavigation( + key: _globalKey, controller: controller), builder: (context, child) { if (controller.snappingSheetContainerSize == null) { return child!; } final rawPosition = - controller.snappingSheetController.currentPosition; + controller.snappingSheetController.isAttached + ? controller.snappingSheetController.currentPosition + : 0; final position = rawPosition / controller.snappingSheetContainerSize!.maxHeight; diff --git a/lib/pages/chat_list/spaces_entry.dart b/lib/pages/chat_list/spaces_entry.dart index 1c1c6b2c..b9e064c2 100644 --- a/lib/pages/chat_list/spaces_entry.dart +++ b/lib/pages/chat_list/spaces_entry.dart @@ -77,6 +77,14 @@ class AllRoomsSpacesEntry extends SpacesEntry { @override bool shouldShowStoriesHeader(BuildContext context) => true; + + @override + bool operator ==(Object other) { + return runtimeType == other.runtimeType; + } + + @override + int get hashCode => runtimeType.hashCode; } // "Direct Chats" entry. @@ -101,6 +109,14 @@ class DirectChatsSpacesEntry extends SpacesEntry { @override bool shouldShowStoriesHeader(BuildContext context) => true; + + @override + bool operator ==(Object other) { + return runtimeType == other.runtimeType; + } + + @override + int get hashCode => runtimeType.hashCode; } // "Groups" entry. @@ -134,6 +150,14 @@ class GroupsSpacesEntry extends SpacesEntry { bool separatedGroup(Room room, List spaces) { return !spaces.any((space) => _roomInsideSpace(room, space)); } + + @override + bool operator ==(Object other) { + return runtimeType == other.runtimeType; + } + + @override + int get hashCode => runtimeType.hashCode; } // All rooms associated with a specific space. @@ -179,12 +203,12 @@ class SpaceSpacesEntry extends SpacesEntry { @override bool stillValid(BuildContext context) => Matrix.of(context).client.getRoomById(space.id) != null; -} -// Produces a "rough equivalence" for maintaining the current spaces index. -bool spacesEntryRoughEquivalence(SpacesEntry a, SpacesEntry b) { - if ((a is SpaceSpacesEntry) && (b is SpaceSpacesEntry)) { - return a.space.id == b.space.id; + @override + bool operator ==(Object other) { + return hashCode == other.hashCode; } - return a == b; + + @override + int get hashCode => space.id.hashCode; } diff --git a/lib/widgets/content_banner.dart b/lib/widgets/content_banner.dart index 1ceabbc5..d58c32be 100644 --- a/lib/widgets/content_banner.dart +++ b/lib/widgets/content_banner.dart @@ -66,6 +66,7 @@ class ContentBanner extends StatelessWidget { imageUrl: src.toString(), height: 300, fit: BoxFit.cover, + errorWidget: (c, m, e) => Icon(defaultIcon, size: 200), ); }) : Icon(defaultIcon, size: 200),