From 49f0a5bde876988a57732d5a68c9014bda398356 Mon Sep 17 00:00:00 2001 From: TheOneWithTheBraid Date: Wed, 2 Mar 2022 20:28:16 +0100 Subject: [PATCH] feat: add context menu actions for desktops Related: #849 Signed-off-by: TheOneWithTheBraid --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat_list/chat_list_item.dart | 366 +++++++++++++--------- lib/pages/chat_list/chat_list_view.dart | 394 ++++++++++++------------ lib/utils/contextual_actions.dart | 78 +++++ pubspec.lock | 14 + pubspec.yaml | 1 + web/index.html | 2 +- 7 files changed, 513 insertions(+), 345 deletions(-) create mode 100644 lib/utils/contextual_actions.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index bb70d735..f4f385c7 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2750,5 +2750,6 @@ "experimentalVideoCalls": "Experimental video calls", "@experimentalVideoCalls": {}, "emailOrUsername": "Email or username", - "@emailOrUsername": {} + "@emailOrUsername": {}, + "select": "Select" } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 16ab0912..7eefedf1 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/contextual_actions.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -6,6 +7,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:pedantic/pedantic.dart'; import 'package:vrouter/vrouter.dart'; +import 'package:contextmenu/contextmenu.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; @@ -155,171 +157,235 @@ class ChatListItem extends StatelessWidget { ? 20.0 : 14.0 : 0.0; - return ListTile( - selected: selected || activeChat, - selectedTileColor: selected - ? Theme.of(context).primaryColor.withAlpha(100) - : Theme.of(context).secondaryHeaderColor, - onLongPress: onLongPress as void Function()?, - leading: selected - ? SizedBox( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Material( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(Avatar.defaultSize), - child: const Icon(Icons.check, color: Colors.white), + return ContextMenuArea( + builder: (c) { + // important to separate between [c] and [context] + final provider = ContextualActions.of(context); + if (provider.actions.isNotEmpty) { + return provider.buildContextMenu(c) + ..insert( + 0, + ListTile( + title: Text(L10n.of(context)!.select), + leading: const Icon(Icons.select_all), + onTap: () { + Navigator.of(c).pop(); + onLongPress?.call(); + }, ), - ) - : Avatar( - mxContent: room.avatar, - name: room.displayname, - onTap: onLongPress as void Function()?, + ); + } else { + return [ + ListTile( + title: Text(L10n.of(context)!.select), + leading: const Icon(Icons.select_all), + onTap: () { + Navigator.of(c).pop(); + onLongPress?.call(); + }, ), - title: Row( - children: [ - Expanded( - child: Text( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: TextStyle( - fontWeight: FontWeight.bold, - color: unread - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).textTheme.bodyText1!.color, + /* + if (controller.spaces.isNotEmpty) + ContextualAction( + label: L10n.of(context)!.addToSpace, + icon: const Icon(Icons.group_work_outlined), + action: controller.addOrRemoveToSpace, + ), + ContextualAction( + label: L10n.of(context)!.toggleUnread, + icon: Icon(controller.anySelectedRoomNotMarkedUnread + ? Icons.mark_chat_read_outlined + : Icons.mark_chat_unread_outlined), + action: controller.toggleUnread, + ), + ContextualAction( + label: L10n.of(context)!.toggleFavorite, + icon: Icon(controller.anySelectedRoomNotFavorite + ? Icons.push_pin_outlined + : Icons.push_pin), + action: controller.toggleFavouriteRoom, + ), + ContextualAction( + icon: Icon(controller.anySelectedRoomNotMuted + ? Icons.notifications_off_outlined + : Icons.notifications_outlined), + label: L10n.of(context)!.toggleMuted, + action: controller.toggleMuted, + ), + ContextualAction( + icon: const Icon(Icons.delete_outlined), + label: L10n.of(context)!.archive, + action: controller.archiveAction, + ), */ + ]; + } + }, + child: ListTile( + selected: selected || activeChat, + selectedTileColor: selected + ? Theme.of(context).primaryColor.withAlpha(100) + : Theme.of(context).secondaryHeaderColor, + onLongPress: onLongPress as void Function()?, + leading: selected + ? SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Material( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(Avatar.defaultSize), + child: const Icon(Icons.check, color: Colors.white), + ), + ) + : Avatar( + mxContent: room.avatar, + name: room.displayname, + onTap: onLongPress as void Function()?, + ), + title: Row( + children: [ + Expanded( + child: Text( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + fontWeight: FontWeight.bold, + color: unread + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).textTheme.bodyText1!.color, + ), ), ), - ), - if (isMuted) - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.notifications_off_outlined, - size: 16, + if (isMuted) + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Icon( + Icons.notifications_off_outlined, + size: 16, + ), + ), + if (room.isFavourite) + Padding( + padding: EdgeInsets.only( + right: room.notificationCount > 0 ? 4.0 : 0.0), + child: Icon( + Icons.push_pin_outlined, + size: 16, + color: Theme.of(context).colorScheme.secondary, + ), ), - ), - if (room.isFavourite) Padding( - padding: EdgeInsets.only( - right: room.notificationCount > 0 ? 4.0 : 0.0), - child: Icon( - Icons.push_pin_outlined, - size: 16, - color: Theme.of(context).colorScheme.secondary, + padding: const EdgeInsets.only(left: 4.0), + child: Text( + room.timeCreated.localizedTimeShort(context), + style: TextStyle( + fontSize: 13, + color: unread + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).textTheme.bodyText2!.color, + ), ), ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - room.timeCreated.localizedTimeShort(context), - style: TextStyle( - fontSize: 13, - color: unread - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).textTheme.bodyText2!.color, - ), - ), - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (typingText.isEmpty && - ownMessage && - room.lastEvent!.status.isSending) ...[ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - const SizedBox(width: 4), ], - AnimatedContainer( - width: typingText.isEmpty ? 0 : 18, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - duration: const Duration(milliseconds: 300), - curve: Curves.bounceInOut, - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.edit_outlined, - color: Theme.of(context).colorScheme.secondary, - size: 14, + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (typingText.isEmpty && + ownMessage && + room.lastEvent!.status.isSending) ...[ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + const SizedBox(width: 4), + ], + AnimatedContainer( + width: typingText.isEmpty ? 0 : 18, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + duration: const Duration(milliseconds: 300), + curve: Curves.bounceInOut, + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.edit_outlined, + color: Theme.of(context).colorScheme.secondary, + size: 14, + ), ), - ), - Expanded( - child: typingText.isNotEmpty - ? Text( - typingText, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - softWrap: false, - ) - : Text( - room.membership == Membership.invite - ? L10n.of(context)!.youAreInvitedToThisChat - : room.lastEvent?.getLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - withSenderNamePrefix: !room.isDirectChat || - room.directChatMatrixID != - room.lastEvent?.senderId, - ) ?? - L10n.of(context)!.emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: unread - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).textTheme.bodyText2!.color, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, - ), - ), - ), - const SizedBox(width: 8), - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.bounceInOut, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBubbleSize, - width: - room.notificationCount == 0 && !unread && !room.hasNewMessages - ? 0 - : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + - 9, - decoration: BoxDecoration( - color: room.highlightCount > 0 - ? Colors.red - : room.notificationCount > 0 - ? Theme.of(context).primaryColor - : Theme.of(context).primaryColor.withAlpha(100), - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: room.notificationCount > 0 + Expanded( + child: typingText.isNotEmpty ? Text( - room.notificationCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 13, + typingText, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, ), + softWrap: false, ) - : Container(), + : Text( + room.membership == Membership.invite + ? L10n.of(context)!.youAreInvitedToThisChat + : room.lastEvent?.getLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + withSenderNamePrefix: !room.isDirectChat || + room.directChatMatrixID != + room.lastEvent?.senderId, + ) ?? + L10n.of(context)!.emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: unread + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).textTheme.bodyText2!.color, + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), ), - ), - ], + const SizedBox(width: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.bounceInOut, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBubbleSize, + width: + room.notificationCount == 0 && !unread && !room.hasNewMessages + ? 0 + : (unreadBubbleSize - 9) * + room.notificationCount.toString().length + + 9, + decoration: BoxDecoration( + color: room.highlightCount > 0 + ? Colors.red + : room.notificationCount > 0 + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColor.withAlpha(100), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + ), + child: Center( + child: room.notificationCount > 0 + ? Text( + room.notificationCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + ), + ) + : Container(), + ), + ), + ], + ), + onTap: () => clickAction(context), ), - onTap: () => clickAction(context), ); } } diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index b712e579..b26e07d4 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fluffychat/utils/contextual_actions.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -25,214 +26,221 @@ 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, + return ContextualActions( + child: Builder(builder: (context) { + return StreamBuilder( + stream: Matrix.of(context).onShareContentChanged.stream, + builder: (context, __) { + final selectMode = controller.selectMode; + if (selectMode == SelectMode.select) { + ContextualActions.of(context).actions.addAll({ + if (controller.spaces.isNotEmpty) + ContextualAction( + label: L10n.of(context)!.addToSpace, + icon: const Icon(Icons.group_work_outlined), + action: controller.addOrRemoveToSpace, + ), + ContextualAction( + label: L10n.of(context)!.toggleUnread, + icon: Icon(controller.anySelectedRoomNotMarkedUnread + ? Icons.mark_chat_read_outlined + : Icons.mark_chat_unread_outlined), + action: controller.toggleUnread, ), - leading: selectMode == SelectMode.normal - ? Matrix.of(context).isMultiAccount - ? ClientChooserButton(controller) - : 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) + ContextualAction( + label: L10n.of(context)!.toggleFavorite, + icon: Icon(controller.anySelectedRoomNotFavorite + ? Icons.push_pin_outlined + : Icons.push_pin), + action: controller.toggleFavouriteRoom, + ), + ContextualAction( + icon: Icon(controller.anySelectedRoomNotMuted + ? Icons.notifications_off_outlined + : Icons.notifications_outlined), + label: L10n.of(context)!.toggleMuted, + action: controller.toggleMuted, + ), + ContextualAction( + icon: const Icon(Icons.delete_outlined), + label: L10n.of(context)!.archive, + action: controller.archiveAction, + ), + }); + } + 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: selectMode == SelectMode.normal + ? Matrix.of(context).isMultiAccount + ? ClientChooserButton(controller) + : 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 + ? ContextualActions.of(context).buildAppBar() + : [ 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, - ), - ] - : [ - 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, + icon: const Icon(Icons.search_outlined), + tooltip: L10n.of(context)!.search, onPressed: () => - VRouter.of(context).to('/stories/create'), + VRouter.of(context).to('/search'), ), - 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), - ], - ), + if (selectMode == SelectMode.normal) + IconButton( + icon: const Icon(Icons.camera_alt_outlined), + tooltip: L10n.of(context)!.addToStory, + onPressed: () => + VRouter.of(context).to('/stories/create'), ), - 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), - ], + 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.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.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.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.share_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.inviteContact), - ], + 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.archive, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.archive_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.archive), - ], + 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.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.settings), - ], + PopupMenuItem( + value: PopupMenuAction.archive, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.archive_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.archive), + ], + ), ), - ), - ], - ), - ], - 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: 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, + 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: 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)), + ]), + floatingActionButton: selectMode == SelectMode.normal + ? FloatingActionButton.extended( + isExtended: controller.scrolledToTop, + onPressed: () => + VRouter.of(context).to('/newprivatechat'), + icon: const Icon(CupertinoIcons.chat_bubble), + label: Text(L10n.of(context)!.newChat), + ) + : null, + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + if (controller.spaces.isNotEmpty && + controller.selectedRoomIds.isEmpty) + SpacesBottomBar(controller), + ], ), - Expanded(child: _ChatListViewBody(controller)), - ]), - floatingActionButton: selectMode == SelectMode.normal - ? FloatingActionButton.extended( - isExtended: controller.scrolledToTop, - onPressed: () => - VRouter.of(context).to('/newprivatechat'), - icon: const Icon(CupertinoIcons.chat_bubble), - label: Text(L10n.of(context)!.newChat), - ) - : null, - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - if (controller.spaces.isNotEmpty && - controller.selectedRoomIds.isEmpty) - SpacesBottomBar(controller), - ], ), - ), - ); - }); + ); + }, + ); + }), + ); } } diff --git a/lib/utils/contextual_actions.dart b/lib/utils/contextual_actions.dart new file mode 100644 index 00000000..e66e8124 --- /dev/null +++ b/lib/utils/contextual_actions.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class ContextualActions extends InheritedWidget { + final ContextualActionState _state = ContextualActionState(); + + ContextualActions({Key? key, required Widget child}) + : super(key: key, child: child); + + static ContextualActions of(BuildContext context) { + final ContextualActions? result = + context.dependOnInheritedWidgetOfExactType(); + assert(result != null, 'No ContextualActions found in context'); + return result!; + } + + Set get actions => _state.actions; + set actions(Set actions) => _state.actions = actions; + + List buildAppBar() => _state.buildAppBar(); + List buildContextMenu(BuildContext context) => + _state.buildContextMenu(context); + + @override + bool updateShouldNotify(covariant ContextualActions oldWidget) => + _state != oldWidget._state; +} + +class ContextualActionState { + /// the currently set [ContextualActions] + /// + /// use [replace] instead of `=` operator + Set actions = {}; + Set contextMenuOnly = {}; + + /// unregisters all present actions and adds riven [newActions] + void replace(Iterable newActions) { + actions.removeWhere((element) => true); + actions.addAll(newActions); + } + + /// buildseach item of [actions] as [IconButton] for use in AppBar + List buildAppBar() { + return actions + .map( + (action) => IconButton( + icon: action.icon, + tooltip: action.label, + onPressed: action.action, + ), + ) + .toList(); + } + + /// builds each item of [actions] as [ListTile] for use in a [ContextMenuArea] + List buildContextMenu(BuildContext context) { + return actions + .map( + (action) => ListTile( + leading: action.icon, + title: Text(action.label), + onTap: () { + Navigator.of(context).pop(); + action.action.call(); + }, + ), + ) + .toList(); + } +} + +class ContextualAction { + final VoidCallback action; + final Widget icon; + final String label; + + const ContextualAction( + {required this.action, required this.icon, required this.label}); +} diff --git a/pubspec.lock b/pubspec.lock index d826f033..522c5c64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.0" + after_layout: + dependency: transitive + description: + name: after_layout + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" analyzer: dependency: transitive description: @@ -241,6 +248,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + contextmenu: + dependency: "direct main" + description: + name: contextmenu + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" convert: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 19be2a89..605346b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: chewie: ^1.2.2 collection: ^1.15.0-nullsafety.4 connectivity_plus: ^2.2.0 + contextmenu: ^3.0.0 cupertino_icons: any desktop_drop: ^0.3.2 desktop_lifecycle: ^0.1.0 diff --git a/web/index.html b/web/index.html index fa9257e2..18fbfd08 100644 --- a/web/index.html +++ b/web/index.html @@ -32,7 +32,7 @@ - +