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 d6e03179..544ff6c5 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