diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 8c508790f5..491865f7c5 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -35,6 +35,7 @@ import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; abstract class AppRoutes { static FutureOr loggedInRedirect( @@ -318,15 +319,25 @@ abstract class AppRoutes { ), GoRoute( path: ':roomid', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - ChatPage( - roomId: state.pathParameters['roomid']!, - shareText: state.uri.queryParameters['body'], - eventId: state.uri.queryParameters['event'], - ), - ), + pageBuilder: (context, state) { + final body = state.uri.queryParameters['body']; + var shareItems = state.extra is List + ? state.extra as List + : null; + if (body != null && body.isNotEmpty) { + shareItems ??= []; + shareItems.add(TextShareItem(body)); + } + return defaultPageBuilder( + context, + state, + ChatPage( + roomId: state.pathParameters['roomid']!, + shareItems: shareItems, + eventId: state.uri.queryParameters['event'], + ), + ); + }, redirect: loggedOutRedirect, routes: [ GoRoute( diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index fce1d38a4a..1fee3a572b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -32,8 +32,10 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import 'send_file_dialog.dart'; @@ -41,14 +43,14 @@ import 'send_location_dialog.dart'; class ChatPage extends StatelessWidget { final String roomId; - final String? shareText; + final List? shareItems; final String? eventId; const ChatPage({ super.key, required this.roomId, this.eventId, - this.shareText, + this.shareItems, }); @override @@ -69,7 +71,7 @@ class ChatPage extends StatelessWidget { return ChatPageWithRoom( key: Key('chat_page_${roomId}_$eventId'), room: room, - shareText: shareText, + shareItems: shareItems, eventId: eventId, ); } @@ -77,13 +79,13 @@ class ChatPage extends StatelessWidget { class ChatPageWithRoom extends StatefulWidget { final Room room; - final String? shareText; + final List? shareItems; final String? eventId; const ChatPageWithRoom({ super.key, required this.room, - this.shareText, + this.shareItems, this.eventId, }); @@ -224,18 +226,42 @@ class ChatController extends State void _loadDraft() async { final prefs = await SharedPreferences.getInstance(); - final draft = widget.shareText ?? prefs.getString('draft_$roomId'); + final draft = prefs.getString('draft_$roomId'); if (draft != null && draft.isNotEmpty) { sendController.text = draft; } } + void _shareItems([_]) { + final shareItems = widget.shareItems; + if (shareItems == null || shareItems.isEmpty) return; + for (final item in shareItems) { + if (item is FileShareItem) continue; + if (item is TextShareItem) room.sendTextEvent(item.value); + if (item is ContentShareItem) room.sendEvent(item.value); + } + final files = shareItems + .whereType() + .map((item) => item.value) + .toList(); + if (files.isEmpty) return; + showAdaptiveDialog( + context: context, + builder: (c) => SendFileDialog( + files: files, + room: room, + outerContext: context, + ), + ); + } + @override void initState() { scrollController.addListener(_updateScrollController); inputFocus.addListener(_inputFocusListener); _loadDraft(); + WidgetsBinding.instance.addPostFrameCallback(_shareItems); super.initState(); _displayChatDetailsColumn = ValueNotifier( Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? @@ -821,17 +847,17 @@ class ChatController extends State } void forwardEventsAction() async { - if (selectedEvents.length == 1) { - Matrix.of(context).shareContent = - selectedEvents.first.getDisplayEvent(timeline!).content; - } else { - Matrix.of(context).shareContent = { - 'msgtype': 'm.text', - 'body': _getSelectedEventString(), - }; - } + if (selectedEvents.isEmpty) return; + await showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: selectedEvents + .map((event) => ContentShareItem(event.content)) + .toList(), + ), + ); + if (!mounted) return; setState(() => selectedEvents.clear()); - context.go('/rooms'); } void sendAgainAction() { diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 22f1982f7f..cc3e2d26ab 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -15,14 +15,15 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat/send_file_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; import 'package:fluffychat/utils/show_update_snackbar.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; import '../../utils/url_launcher.dart'; @@ -34,11 +35,6 @@ import '../bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; -enum SelectMode { - normal, - share, -} - enum PopupMenuAction { settings, invite, @@ -191,42 +187,6 @@ class ChatListController extends State setActiveSpace(room.id); return; } - // Share content into this room - final shareContent = Matrix.of(context).shareContent; - if (shareContent != null) { - final shareFile = shareContent.tryGet('file'); - if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file' && - shareFile != null) { - await showDialog( - context: context, - useRootNavigator: false, - builder: (c) => SendFileDialog( - files: [shareFile], - room: room, - outerContext: context, - ), - ); - Matrix.of(context).shareContent = null; - } else { - final consent = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).forward, - message: L10n.of(context).forwardMessageTo( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - ), - okLabel: L10n.of(context).forward, - cancelLabel: L10n.of(context).cancel, - ); - if (consent == OkCancelResult.cancel) { - Matrix.of(context).shareContent = null; - return; - } - if (consent == OkCancelResult.ok) { - room.sendEvent(shareContent); - Matrix.of(context).shareContent = null; - } - } - } context.go('/rooms/${room.id}'); } @@ -420,53 +380,27 @@ class ChatListController extends State String? get activeChat => widget.activeChat; - SelectMode get selectMode => Matrix.of(context).shareContent != null - ? SelectMode.share - : SelectMode.normal; - void _processIncomingSharedMedia(List files) { if (files.isEmpty) return; - if (files.length > 1) { - Logs().w( - 'Received ${files.length} incoming shared media but app can only handle the first one', - ); - } - - // We only handle the first file currently - final sharedMedia = files.first; - - // Handle URIs and Texts, which are also passed in path - if (sharedMedia.type case SharedMediaType.text || SharedMediaType.url) { - return _processIncomingSharedText(sharedMedia.path); - } - - final file = XFile( - sharedMedia.path.replaceFirst('file://', ''), - mimeType: sharedMedia.mimeType, + showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: files + .map( + (file) => switch (file.type) { + SharedMediaType.file => FileShareItem( + XFile( + file.path.replaceFirst('file://', ''), + mimeType: file.mimeType, + ), + ), + _ => TextShareItem(file.path), + }, + ) + .toList(), + ), ); - - Matrix.of(context).shareContent = { - 'msgtype': 'chat.fluffy.shared_file', - 'file': file, - if (sharedMedia.message != null) 'body': sharedMedia.message, - }; - context.go('/rooms'); - } - - void _processIncomingSharedText(String? text) { - if (text == null) return; - if (text.toLowerCase().startsWith(AppConfig.deepLinkPrefix) || - text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) || - (text.toLowerCase().startsWith(AppConfig.schemePrefix) && - !RegExp(r'\s').hasMatch(text))) { - return _processIncomingUris(text); - } - Matrix.of(context).shareContent = { - 'msgtype': 'm.text', - 'body': text, - }; - context.go('/rooms'); } void _processIncomingUris(String? text) async { @@ -871,12 +805,6 @@ class ChatListController extends State } } - void cancelAction() { - if (selectMode == SelectMode.share) { - setState(() => Matrix.of(context).shareContent = null); - } - } - void setActiveFilter(ActiveFilter filter) { setState(() { activeFilter = filter; diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 5d878be9a1..2c300cfcc4 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -21,112 +21,84 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - final selectMode = controller.selectMode; - return SliverAppBar( floating: true, toolbarHeight: 72, - pinned: - FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, - scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, - backgroundColor: - selectMode == SelectMode.normal ? Colors.transparent : null, + pinned: FluffyThemes.isColumnMode(context), + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, automaticallyImplyLeading: false, - leading: selectMode == SelectMode.normal - ? null - : IconButton( - tooltip: L10n.of(context).cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelAction, - color: theme.colorScheme.primary, - ), - title: selectMode == SelectMode.share - ? Text( - L10n.of(context).share, - key: const ValueKey(SelectMode.share), - ) - : TextField( - controller: controller.searchController, - focusNode: controller.searchFocusNode, - textInputAction: TextInputAction.search, - onChanged: (text) => controller.onSearchEnter( - text, - globalSearch: globalSearch, - ), - decoration: InputDecoration( - filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - contentPadding: EdgeInsets.zero, - hintText: L10n.of(context).searchChatsRooms, - hintStyle: TextStyle( + title: TextField( + controller: controller.searchController, + focusNode: controller.searchFocusNode, + textInputAction: TextInputAction.search, + onChanged: (text) => controller.onSearchEnter( + text, + globalSearch: globalSearch, + ), + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context).searchChatsRooms, + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: controller.isSearchMode + ? IconButton( + tooltip: L10n.of(context).cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelSearch, color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, + ) + : IconButton( + onPressed: controller.startSearch, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: controller.isSearchMode - ? IconButton( - tooltip: L10n.of(context).cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelSearch, - color: theme.colorScheme.onPrimaryContainer, - ) - : IconButton( - onPressed: controller.startSearch, - icon: Icon( - Icons.search_outlined, - color: theme.colorScheme.onPrimaryContainer, + suffixIcon: controller.isSearchMode && globalSearch + ? controller.isSearching + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : TextButton.icon( + onPressed: controller.setServer, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(99), ), + textStyle: const TextStyle(fontSize: 12), ), - suffixIcon: controller.isSearchMode && globalSearch - ? controller.isSearching - ? const Padding( - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12, - ), - child: SizedBox.square( - dimension: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ), - ) - : TextButton.icon( - onPressed: controller.setServer, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(99), - ), - textStyle: const TextStyle(fontSize: 12), - ), - icon: const Icon(Icons.edit_outlined, size: 16), - label: Text( - controller.searchServer ?? - Matrix.of(context).client.homeserver!.host, - maxLines: 2, - ), - ) - : SizedBox( - width: 0, - child: ClientChooserButton(controller), + icon: const Icon(Icons.edit_outlined, size: 16), + label: Text( + controller.searchServer ?? + Matrix.of(context).client.homeserver!.host, + maxLines: 2, ), - ), - ), - actions: selectMode == SelectMode.share - ? [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + ) + : SizedBox( + width: 0, + child: ClientChooserButton(controller), ), - child: ClientChooserButton(controller), - ), - ] - : null, + ), + ), ); } diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 5d24948133..aecb98c150 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -22,140 +22,124 @@ class ChatListView extends StatelessWidget { @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - return StreamBuilder( - stream: Matrix.of(context).onShareContentChanged.stream, - builder: (_, __) { - final selectMode = controller.selectMode; - return PopScope( - canPop: controller.selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeSpaceId == null, - onPopInvokedWithResult: (pop, _) { - if (pop) return; - if (controller.activeSpaceId != null) { - controller.clearActiveSpace(); - return; - } - final selMode = controller.selectMode; - if (controller.isSearchMode) { - controller.cancelSearch(); - return; - } - if (selMode != SelectMode.normal) { - controller.cancelAction(); - return; - } - }, - child: Row( - children: [ - if (FluffyThemes.isColumnMode(context) && - controller.widget.displayNavigationRail) ...[ - StreamBuilder( - key: ValueKey( - client.userID.toString(), - ), - stream: client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) { - final allSpaces = Matrix.of(context) - .client - .rooms - .where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); - - return SizedBox( - width: FluffyThemes.navRailWidth, - child: ListView.builder( - scrollDirection: Axis.vertical, - itemCount: rootSpaces.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return NaviRailItem( - isSelected: controller.activeSpaceId == null, - onTap: controller.clearActiveSpace, - icon: const Icon(Icons.forum_outlined), - selectedIcon: const Icon(Icons.forum), - toolTip: L10n.of(context).chats, - unreadBadgeFilter: (room) => true, - ); - } - i--; - if (i == rootSpaces.length) { - return NaviRailItem( - isSelected: false, - onTap: () => context.go('/rooms/newspace'), - icon: const Icon(Icons.add), - toolTip: L10n.of(context).createNewSpace, - ); - } - final space = rootSpaces[i]; - final displayname = - rootSpaces[i].getLocalizedDisplayname( - MatrixLocals(L10n.of(context)), - ); - final spaceChildrenIds = - space.spaceChildren.map((c) => c.roomId).toSet(); - return NaviRailItem( - toolTip: displayname, - isSelected: controller.activeSpaceId == space.id, - onTap: () => - controller.setActiveSpace(rootSpaces[i].id), - unreadBadgeFilter: (room) => - spaceChildrenIds.contains(room.id), - icon: Avatar( - mxContent: rootSpaces[i].avatar, - name: displayname, - size: 32, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - ), - ); - }, + return PopScope( + canPop: !controller.isSearchMode && controller.activeSpaceId == null, + onPopInvokedWithResult: (pop, _) { + if (pop) return; + if (controller.activeSpaceId != null) { + controller.clearActiveSpace(); + return; + } + if (controller.isSearchMode) { + controller.cancelSearch(); + return; + } + }, + child: Row( + children: [ + if (FluffyThemes.isColumnMode(context) && + controller.widget.displayNavigationRail) ...[ + StreamBuilder( + key: ValueKey( + client.userID.toString(), + ), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { + final allSpaces = Matrix.of(context) + .client + .rooms + .where((room) => room.isSpace); + final rootSpaces = allSpaces + .where( + (space) => !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), ), - ); - }, - ), - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: GestureDetector( - onTap: FocusManager.instance.primaryFocus?.unfocus, - excludeFromSemantics: true, - behavior: HitTestBehavior.translucent, - child: Scaffold( - body: ChatListViewBody(controller), - floatingActionButton: selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeSpaceId == null - ? FloatingActionButton.extended( - onPressed: () => - context.go('/rooms/newprivatechat'), - icon: const Icon(Icons.add_outlined), - label: Text( - L10n.of(context).chat, - overflow: TextOverflow.fade, - ), - ) - : const SizedBox.shrink(), + ) + .toList(); + + return SizedBox( + width: FluffyThemes.navRailWidth, + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: rootSpaces.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return NaviRailItem( + isSelected: controller.activeSpaceId == null, + onTap: controller.clearActiveSpace, + icon: const Icon(Icons.forum_outlined), + selectedIcon: const Icon(Icons.forum), + toolTip: L10n.of(context).chats, + unreadBadgeFilter: (room) => true, + ); + } + i--; + if (i == rootSpaces.length) { + return NaviRailItem( + isSelected: false, + onTap: () => context.go('/rooms/newspace'), + icon: const Icon(Icons.add), + toolTip: L10n.of(context).createNewSpace, + ); + } + final space = rootSpaces[i]; + final displayname = rootSpaces[i].getLocalizedDisplayname( + MatrixLocals(L10n.of(context)), + ); + final spaceChildrenIds = + space.spaceChildren.map((c) => c.roomId).toSet(); + return NaviRailItem( + toolTip: displayname, + isSelected: controller.activeSpaceId == space.id, + onTap: () => + controller.setActiveSpace(rootSpaces[i].id), + unreadBadgeFilter: (room) => + spaceChildrenIds.contains(room.id), + icon: Avatar( + mxContent: rootSpaces[i].avatar, + name: displayname, + size: 32, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + ), + ); + }, ), - ), + ); + }, + ), + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: GestureDetector( + onTap: FocusManager.instance.primaryFocus?.unfocus, + excludeFromSemantics: true, + behavior: HitTestBehavior.translucent, + child: Scaffold( + body: ChatListViewBody(controller), + floatingActionButton: !controller.isSearchMode && + controller.activeSpaceId == null + ? FloatingActionButton.extended( + onPressed: () => context.go('/rooms/newprivatechat'), + icon: const Icon(Icons.add_outlined), + label: Text( + L10n.of(context).chat, + overflow: TextOverflow.fade, + ), + ) + : const SizedBox.shrink(), ), - ], + ), ), - ); - }, + ], + ), ); } } diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 9e8cfe82c3..07601ee602 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../utils/matrix_sdk_extensions/event_extension.dart'; class ImageViewer extends StatefulWidget { @@ -20,11 +20,12 @@ class ImageViewer extends StatefulWidget { class ImageViewerController extends State { /// Forward this image to another room. - void forwardAction() { - Matrix.of(widget.outerContext).shareContent = widget.event.content; - Navigator.of(context).pop(); - widget.outerContext.go('/rooms'); - } + void forwardAction() => showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: [ContentShareItem(widget.event.content)], + ), + ); /// Save this file with a system call. void saveFileAction(BuildContext context) => widget.event.saveFile(context); diff --git a/lib/utils/show_scaffold_dialog.dart b/lib/utils/show_scaffold_dialog.dart new file mode 100644 index 0000000000..b29802b286 --- /dev/null +++ b/lib/utils/show_scaffold_dialog.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; + +Future showScaffoldDialog({ + required BuildContext context, + Color? barrierColor, + Color? containerColor, + double maxWidth = 480, + double maxHeight = 720, + required Widget Function(BuildContext context) builder, +}) => + showDialog( + context: context, + builder: FluffyThemes.isColumnMode(context) + ? (context) => Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: containerColor ?? + Theme.of(context).scaffoldBackgroundColor, + ), + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.all(16), + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + child: builder(context), + ), + ) + : builder, + ); diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 30490a31c9..891fff9f9a 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -176,18 +176,6 @@ class MatrixState extends State with WidgetsBindingObserver { Client? getClientByName(String name) => widget.clients.firstWhereOrNull((c) => c.clientName == name); - Map? get shareContent => _shareContent; - - set shareContent(Map? content) { - _shareContent = content; - onShareContentChanged.add(_shareContent); - } - - Map? _shareContent; - - final StreamController?> onShareContentChanged = - StreamController.broadcast(); - final onRoomKeyRequestSub = {}; final onKeyVerificationRequestSub = {}; final onNotification = {}; diff --git a/lib/widgets/share_scaffold_dialog.dart b/lib/widgets/share_scaffold_dialog.dart new file mode 100644 index 0000000000..e70bb5fedb --- /dev/null +++ b/lib/widgets/share_scaffold_dialog.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +abstract class ShareItem {} + +class TextShareItem extends ShareItem { + final String value; + TextShareItem(this.value); +} + +class ContentShareItem extends ShareItem { + final Map value; + ContentShareItem(this.value); +} + +class FileShareItem extends ShareItem { + final XFile value; + FileShareItem(this.value); +} + +class ShareScaffoldDialog extends StatefulWidget { + final List items; + + const ShareScaffoldDialog({required this.items, super.key}); + + @override + State createState() => _ShareScaffoldDialogState(); +} + +class _ShareScaffoldDialogState extends State { + final TextEditingController _filterController = TextEditingController(); + + String? selectedRoomId; + bool isLoading = false; + + void _toggleRoom(String roomId) { + setState(() { + selectedRoomId = roomId; + }); + } + + void _forwardAction() async { + final roomId = selectedRoomId; + if (roomId == null) { + throw Exception( + 'Started forward action before room was selected. This should never happen.', + ); + } + context.pop(); + context.go('/rooms/$roomId', extra: widget.items); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final rooms = Matrix.of(context) + .client + .rooms + .where( + (room) => + room.canSendDefaultMessages && + !room.isSpace && + room.membership == Membership.join, + ) + .toList(); + final filter = _filterController.text.trim().toLowerCase(); + return Scaffold( + appBar: AppBar( + leading: Center(child: CloseButton(onPressed: context.pop)), + title: Text(L10n.of(context).share), + ), + body: CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + toolbarHeight: 72, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + title: TextField( + controller: _filterController, + onChanged: (_) => setState(() {}), + textInputAction: TextInputAction.search, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context).search, + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + ), + ), + SliverList.builder( + itemCount: rooms.length, + itemBuilder: (context, i) { + final room = rooms[i]; + final displayname = room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)), + ); + final value = selectedRoomId == room.id; + final filterOut = !displayname.toLowerCase().contains(filter); + if (!value && filterOut) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Opacity( + opacity: filterOut ? 0.5 : 1, + child: CheckboxListTile.adaptive( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + secondary: Avatar( + mxContent: room.avatar, + name: displayname, + size: Avatar.defaultSize * 0.75, + ), + title: Text(displayname), + value: value, + onChanged: filterOut || isLoading + ? null + : (_) => _toggleRoom(room.id), + checkboxShape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + ), + ), + ); + }, + ), + ], + ), + bottomNavigationBar: AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: selectedRoomId == null && !isLoading + ? const SizedBox.shrink() + : Material( + elevation: 8, + shadowColor: theme.appBarTheme.shadowColor, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: isLoading ? null : _forwardAction, + child: isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context).forward), + ), + ), + ), + ), + ); + } +}