From 44ffaa1b41e99a186f957217702c3468a152c3b1 Mon Sep 17 00:00:00 2001 From: TheOneWithTheBraid Date: Sat, 2 Apr 2022 16:18:36 +0200 Subject: [PATCH] feat: improve spaces - support to show spaces in a list - add a beautiful animation This MR makes Spaces much easier to use on desktops and allows to better find the right space in case they have no avatar. There will be another MR builting on this work as soon as https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58 is merged. Signed-off-by: TheOneWithTheBraid --- assets/l10n/intl_en.arb | 1 + lib/pages/chat_list/chat_list.dart | 32 +- lib/pages/chat_list/chat_list_view.dart | 446 ++++++++++-------- lib/pages/chat_list/spaces_bottom_bar.dart | 138 ++++-- lib/pages/chat_list/spaces_drawer.dart | 83 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 70 ++- pubspec.yaml | 7 + 8 files changed, 499 insertions(+), 280 deletions(-) create mode 100644 lib/pages/chat_list/spaces_drawer.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fdf8f7eb..e4bcfcbd 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1311,6 +1311,7 @@ "type": "text", "placeholders": {} }, + "showSpaces": "Show spaces list", "loadMore": "Load moreā€¦", "@loadMore": { "type": "text", diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index a77699d5..04292f45 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -8,11 +8,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:snapping_sheet/snapping_sheet.dart'; import 'package:uni_links/uni_links.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; +import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -40,7 +42,7 @@ class ChatList extends StatefulWidget { ChatListController createState() => ChatListController(); } -class ChatListController extends State { +class ChatListController extends State with TickerProviderStateMixin { StreamSubscription? _intentDataStreamSubscription; StreamSubscription? _intentFileStreamSubscription; @@ -54,6 +56,8 @@ class ChatListController extends State { return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id; } + BoxConstraints? snappingSheetContainerSize; + String? get activeSpaceId => activeSpacesEntry.getSpace(context)?.id; final ScrollController scrollController = ScrollController(); @@ -61,6 +65,10 @@ class ChatListController extends State { final StreamController _clientStream = StreamController.broadcast(); + SnappingSheetController snappingSheetController = SnappingSheetController(); + + ScrollController snappingSheetScrollContentController = ScrollController(); + Stream get clientStream => _clientStream.stream; void _onScroll() { @@ -72,7 +80,10 @@ class ChatListController extends State { } } - void setActiveSpacesEntry(BuildContext context, SpacesEntry spaceId) { + void setActiveSpacesEntry(BuildContext context, SpacesEntry? spaceId) { + if (snappingSheetController.currentPosition != kSpacesBottomBarHeight) { + snapBackSpacesSheet(); + } setState(() => _activeSpacesEntry = spaceId); } @@ -480,6 +491,8 @@ class ChatListController extends State { VRouter.of(context).to('/rooms'); setState(() { _activeSpacesEntry = null; + snappingSheetController = SnappingSheetController(); + snappingSheetScrollContentController = ScrollController(); selectedRoomIds.clear(); Matrix.of(context).setActiveClient(client); }); @@ -575,6 +588,21 @@ class ChatListController extends State { void _hackyWebRTCFixForWeb() { Matrix.of(context).voipPlugin?.context = context; } + + void snapBackSpacesSheet() { + snappingSheetController.snapToPosition( + const SnappingPosition.pixels( + positionPixels: kSpacesBottomBarHeight, + snappingDuration: Duration(milliseconds: 500), + ), + ); + } + + expandSpaces() { + snappingSheetController.snapToPosition( + const SnappingPosition.factor(positionFactor: 0.5), + ); + } } enum EditBundleAction { addToBundle, removeFromBundle } diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index b9df3abc..e1bfb10b 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -8,6 +8,7 @@ import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; +import 'package:snapping_sheet/snapping_sheet.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -28,203 +29,240 @@ class ChatListView extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: Matrix.of(context).onShareContentChanged.stream, - builder: (_, __) { - final selectMode = controller.selectMode; - return VWidgetGuard( - onSystemPop: (redirector) async { - final selMode = controller.selectMode; - if (selMode != SelectMode.normal) controller.cancelAction(); - 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 + stream: Matrix.of(context).onShareContentChanged.stream, + builder: (_, __) { + final selectMode = controller.selectMode; + final showSpaces = + controller.spaces.isNotEmpty && controller.selectedRoomIds.isEmpty; + return VWidgetGuard( + onSystemPop: (redirector) async { + final selMode = controller.selectMode; + if (selMode != SelectMode.normal) controller.cancelAction(); + if (selMode == SelectMode.select) redirector.stopRedirection(); + }, + child: Scaffold( + appBar: AppBar( + elevation: controller.scrolledToTop ? 0 : null, + actionsIconTheme: IconThemeData( + color: controller.selectedRoomIds.isEmpty ? 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), + : Theme.of(context).colorScheme.primary, ), - body: Column(children: [ - AnimatedContainer( - height: controller.showChatBackupBanner ? 54 : 0, - duration: const Duration(milliseconds: 300), - clipBehavior: Clip.hardEdge, - curve: Curves.bounceInOut, - decoration: const BoxDecoration(), - child: Material( - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: Image.asset( - 'assets/backup.png', - fit: BoxFit.contain, - width: 44, + 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), + ), + body: LayoutBuilder( + builder: (context, size) { + controller.snappingSheetContainerSize = size; + return SnappingSheet( + key: ValueKey(Matrix.of(context).client.userID.toString() + + showSpaces.toString()), + controller: controller.snappingSheetController, + child: Column( + children: [ + AnimatedContainer( + height: controller.showChatBackupBanner ? 54 : 0, + duration: const Duration(milliseconds: 300), + clipBehavior: Clip.hardEdge, + curve: Curves.bounceInOut, + decoration: const BoxDecoration(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: Image.asset( + 'assets/backup.png', + fit: BoxFit.contain, + width: 44, + ), + title: Text(L10n.of(context)!.setupChatBackupNow), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.firstRunBootstrapAction, + ), + ), ), - title: Text(L10n.of(context)!.setupChatBackupNow), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.firstRunBootstrapAction, - ), + Expanded(child: _ChatListViewBody(controller)), + ], ), - ), - Expanded(child: _ChatListViewBody(controller)), - ]), - floatingActionButton: selectMode == SelectMode.normal - ? KeyBoardShortcuts( + initialSnappingPosition: showSpaces + ? const SnappingPosition.pixels( + positionPixels: kSpacesBottomBarHeight) + : const SnappingPosition.factor(positionFactor: 0.0), + snappingPositions: showSpaces + ? const [ + SnappingPosition.pixels( + positionPixels: kSpacesBottomBarHeight), + SnappingPosition.factor(positionFactor: 0.5), + SnappingPosition.factor(positionFactor: 0.9), + ] + : [const SnappingPosition.factor(positionFactor: 0.0)], + sheetBelow: showSpaces + ? SnappingSheetContent( + childScrollController: + controller.snappingSheetScrollContentController, + draggable: true, + child: SpacesBottomBar(controller), + ) + : null, + ); + }, + ), + floatingActionButton: selectMode == SelectMode.normal + ? Padding( + padding: showSpaces + ? const EdgeInsets.only(bottom: 64.0) + : const EdgeInsets.all(0), + child: KeyBoardShortcuts( child: FloatingActionButton.extended( isExtended: controller.scrolledToTop, onPressed: () => @@ -239,20 +277,14 @@ class ChatListView extends StatelessWidget { onKeysPressed: () => VRouter.of(context).to('/newprivatechat'), helpLabel: L10n.of(context)!.newChat, - ) - : null, - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - if (controller.spaces.isNotEmpty && - controller.selectedRoomIds.isEmpty) - SpacesBottomBar(controller), - ], - ), - ), - ); - }); + ), + ) + : null, + bottomNavigationBar: const ConnectionStatusHeader(), + ), + ); + }, + ); } } diff --git a/lib/pages/chat_list/spaces_bottom_bar.dart b/lib/pages/chat_list/spaces_bottom_bar.dart index 61057003..b1f1dcf8 100644 --- a/lib/pages/chat_list/spaces_bottom_bar.dart +++ b/lib/pages/chat_list/spaces_bottom_bar.dart @@ -1,53 +1,135 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:salomon_bottom_bar/salomon_bottom_bar.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/spaces_drawer.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +const kSpacesBottomBarHeight = 56.0; + class SpacesBottomBar extends StatelessWidget { final ChatListController controller; + const SpacesBottomBar(this.controller, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final foundIndex = controller.spacesEntries.indexWhere( - (se) => spacesEntryRoughEquivalence(se, controller.activeSpacesEntry)); - final currentIndex = foundIndex == -1 ? 0 : foundIndex; return Material( - color: Theme.of(context).appBarTheme.backgroundColor, + color: Theme.of(context).navigationBarTheme.backgroundColor, elevation: 6, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppConfig.borderRadius)), + clipBehavior: Clip.hardEdge, child: SafeArea( child: StreamBuilder( - stream: Matrix.of(context).client.onSync.stream.where((sync) => - (sync.rooms?.join?.values.any((r) => - r.state?.any((s) => s.type.startsWith('m.space')) ?? - false) ?? - false) || - (sync.rooms?.leave?.isNotEmpty ?? false)), - builder: (context, snapshot) { - return Container( - height: 56, - alignment: Alignment.center, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SalomonBottomBar( - itemPadding: const EdgeInsets.all(8), - currentIndex: currentIndex, - onTap: (i) => controller.setActiveSpacesEntry( + stream: Matrix.of(context).client.onSync.stream.where((sync) => + (sync.rooms?.join?.values.any((r) => + r.state?.any((s) => s.type.startsWith('m.space')) ?? + false) ?? + false) || + (sync.rooms?.leave?.isNotEmpty ?? false)), + builder: (context, snapshot) { + return SingleChildScrollView( + controller: controller.snappingSheetScrollContentController, + child: AnimatedBuilder( + child: _SpacesBottomNavigation(controller: controller), + builder: (context, child) { + if (controller.snappingSheetContainerSize == null) { + return child!; + } + final rawPosition = + controller.snappingSheetController.currentPosition; + final position = rawPosition / + controller.snappingSheetContainerSize!.maxHeight; + + if (rawPosition <= kSpacesBottomBarHeight) { + return child!; + } else if (position >= 0.5) { + return SpacesDrawer(controller: controller); + } else { + final normalized = (rawPosition - kSpacesBottomBarHeight) / + (controller.snappingSheetContainerSize!.maxHeight - + kSpacesBottomBarHeight) * + 2; + var boxHeight = (1 - normalized) * kSpacesBottomBarHeight; + if (boxHeight < 0) boxHeight = 0; + + return Column( + children: [ + SizedBox( + height: boxHeight, + child: ClipRect( + clipBehavior: Clip.hardEdge, + child: Opacity( + opacity: 1 - normalized, child: child!)), + ), + Opacity( + opacity: normalized, + child: SpacesDrawer(controller: controller), + ), + ], + ); + } + }, + animation: controller.snappingSheetController, + ), + ); + }, + ), + ), + ); + } +} + +class _SpacesBottomNavigation extends StatelessWidget { + final ChatListController controller; + + const _SpacesBottomNavigation({Key? key, required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final currentIndex = controller.activeSpaceId == null + ? 1 + : controller.spaces + .indexWhere((space) => controller.activeSpaceId == space.id) + + 2; + + return Container( + height: 56, + alignment: Alignment.center, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SalomonBottomBar( + itemPadding: const EdgeInsets.all(8), + currentIndex: currentIndex, + onTap: (i) => i == 0 + ? controller.expandSpaces() + : i == 1 + ? controller.setActiveSpacesEntry( + context, + null, + ) + : controller.setActiveSpacesEntry( context, controller.spacesEntries[i], ), - selectedItemColor: Theme.of(context).colorScheme.primary, - items: controller.spacesEntries - .map((entry) => _buildSpacesEntryUI(context, entry)) - .toList(), - ), - ), - ); - }), + selectedItemColor: Theme.of(context).colorScheme.primary, + items: [ + SalomonBottomBarItem( + icon: const Icon(Icons.keyboard_arrow_up), + title: Text(L10n.of(context)!.showSpaces), + ), + ...controller.spacesEntries + .map((space) => _buildSpacesEntryUI(context, space)) + .toList(), + ], + ), ), ); } diff --git a/lib/pages/chat_list/spaces_drawer.dart b/lib/pages/chat_list/spaces_drawer.dart new file mode 100644 index 00000000..298c3ef6 --- /dev/null +++ b/lib/pages/chat_list/spaces_drawer.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'chat_list.dart'; + +class SpacesDrawer extends StatelessWidget { + final ChatListController controller; + + const SpacesDrawer({Key? key, required this.controller}) : super(key: key); + + @override + Widget build(BuildContext context) { + final currentIndex = controller.activeSpaceId == null + ? 0 + : controller.spaces + .indexWhere((space) => controller.activeSpaceId == space.id) + + 1; + + final Map spaceHierarchy = + Map.fromEntries(controller.spacesEntries.map((e) => MapEntry(e, null))); + + // TODO(TheOeWithTheBraid): wait for space hierarchy https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58 + + return WillPopScope( + onWillPop: () async { + controller.snapBackSpacesSheet(); + return false; + }, + child: ListView.builder( + shrinkWrap: true, + itemCount: spaceHierarchy.length, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return ListTile( + selected: currentIndex == index, + leading: const Icon(Icons.keyboard_arrow_down), + title: Text(L10n.of(context)!.allChats), + onTap: () => controller.setActiveSpacesEntry( + context, + null, + ), + ); + } else { + final space = spaceHierarchy.keys.toList()[index]; + final room = space.getSpace(context)!; + return ListTile( + selected: currentIndex == index, + leading: Avatar( + mxContent: room.avatar, + name: space.getName(context), + size: 24, + fontSize: 12, + ), + title: Text(space.getName(context)), + subtitle: room.topic.isEmpty + ? null + : Tooltip( + message: room.topic, + child: Text( + room.topic.replaceAll('\n', ' '), + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + onTap: () => controller.setActiveSpacesEntry( + context, + space, + ), + trailing: IconButton( + icon: const Icon(Icons.edit), + tooltip: L10n.of(context)!.edit, + onPressed: () => controller.editSpace(context, room.id), + ), + ); + } + }, + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0e69f284..62cfaffc 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,7 +12,6 @@ import desktop_lifecycle import device_info_plus_macos import emoji_picker_flutter import file_selector_macos -import flutter_app_badger import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth @@ -36,7 +35,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4b96004d..8922a3dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: adaptive_dialog url: "https://pub.dartlang.org" source: hosted - version: "1.5.0+1" + version: "1.4.0" adaptive_theme: dependency: "direct main" description: @@ -289,14 +289,14 @@ packages: name: dart_webrtc url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.4" dbus: dependency: "direct overridden" description: name: dbus url: "https://pub.dartlang.org" source: hosted - version: "0.7.3" + version: "0.7.2" desktop_drop: dependency: "direct main" description: @@ -483,7 +483,7 @@ packages: name: flutter_app_badger url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.3.0" flutter_app_lock: dependency: "direct main" description: @@ -551,7 +551,7 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "9.4.0" + version: "9.4.1" flutter_local_notifications_linux: dependency: transitive description: @@ -598,7 +598,7 @@ packages: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.2+1" flutter_olm: dependency: "direct main" description: @@ -626,7 +626,7 @@ packages: name: flutter_ringtone_player url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.1.1" flutter_secure_storage: dependency: "direct main" description: @@ -713,7 +713,7 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.5" + version: "0.8.4" frontend_server_client: dependency: transitive description: @@ -851,13 +851,6 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.5" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - url: "https://pub.dartlang.org" - source: hosted version: "0.8.4+11" image_picker_for_web: dependency: transitive @@ -866,13 +859,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.6" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.4+11" image_picker_platform_interface: dependency: transitive description: @@ -957,13 +943,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.8.1" - lint: - dependency: transitive - description: - name: lint - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" lints: dependency: transitive description: @@ -1140,14 +1119,14 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.0" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.3" package_info_plus_macos: dependency: transitive description: @@ -1168,14 +1147,14 @@ packages: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.4" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.4" path: dependency: transitive description: @@ -1273,7 +1252,7 @@ packages: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.4" + version: "9.0.3" permission_handler_platform_interface: dependency: transitive description: @@ -1420,7 +1399,7 @@ packages: name: record url: "https://pub.dartlang.org" source: hosted - version: "3.0.4" + version: "3.0.3" record_platform_interface: dependency: transitive description: @@ -1539,7 +1518,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.2.0" shelf_packages_handler: dependency: transitive description: @@ -1573,6 +1552,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + snapping_sheet: + dependency: "direct main" + description: + path: "." + ref: listenable + resolved-ref: "3da78eea5d222baa1b266c19284acafee090f6be" + url: "https://github.com/TheOneWithTheBraid/snapping_sheet.git" + source: git + version: "3.1.0" source_map_stack_trace: dependency: transitive description: @@ -1607,7 +1595,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.0" stack_trace: dependency: transitive description: @@ -1831,7 +1819,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.6" url_launcher_windows: dependency: transitive description: @@ -1999,14 +1987,14 @@ packages: name: webrtc_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.2" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.5.1" + version: "2.5.0" wkt_parser: dependency: transitive description: @@ -2037,4 +2025,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.16.1 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=2.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0c489064..65d0afa0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,7 @@ dependencies: share: ^2.0.4 shared_preferences: ^2.0.13 slugify: ^2.0.0 + snapping_sheet: ^3.1.0 swipe_to_action: ^0.2.0 uni_links: ^0.5.1 unifiedpush: ^4.0.0 @@ -146,3 +147,9 @@ dependency_overrides: url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git ref: null-safety provider: 5.0.0 + # wating for `Listenable` implementation + # Upstream pull request: https://github.com/AdamJonsson/snapping_sheet/pull/84 + snapping_sheet: + git: + url: https://github.com/TheOneWithTheBraid/snapping_sheet.git + ref: listenable