diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index c8ef099a0..48c43b238 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,130 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:badges/badges.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; -import 'package:fluffychat/pages/chat/encryption_button.dart'; -import 'package:fluffychat/pages/chat/pinned_events.dart'; -import 'package:fluffychat/pages/chat/reactions_picker.dart'; -import 'package:fluffychat/pages/chat/reply_display.dart'; -import 'package:fluffychat/pages/chat/tombstone_display.dart'; -import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; -import 'package:fluffychat/widgets/connection_status_header.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/unread_rooms_badge.dart'; -import '../../utils/stream_extension.dart'; -import 'chat_emoji_picker.dart'; -import 'chat_input_row.dart'; - -enum _EventContextAction { info, report } +import 'package:fluffychat/pages/chat/chat_view_frame.dart'; +import 'package:fluffychat/pages/chat/chat_view_scaffold.dart'; class ChatView extends StatelessWidget { final ChatController controller; const ChatView(this.controller, {super.key}); - List _appBarActions(BuildContext context) { - if (controller.selectMode) { - return [ - if (controller.canEditSelectedEvents) - IconButton( - icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context)!.edit, - onPressed: controller.editSelectedEventAction, - ), - IconButton( - icon: const Icon(Icons.copy_outlined), - tooltip: L10n.of(context)!.copy, - onPressed: controller.copyEventsAction, - ), - if (controller.canSaveSelectedEvent) - // Use builder context to correctly position the share dialog on iPad - Builder( - builder: (context) => IconButton( - icon: Icon(Icons.adaptive.share), - tooltip: L10n.of(context)!.share, - onPressed: () => controller.saveSelectedEvent(context), - ), - ), - if (controller.canPinSelectedEvents) - IconButton( - icon: const Icon(Icons.push_pin_outlined), - onPressed: controller.pinEvent, - tooltip: L10n.of(context)!.pinMessage, - ), - if (controller.canRedactSelectedEvents) - IconButton( - icon: const Icon(Icons.delete_outlined), - tooltip: L10n.of(context)!.redactMessage, - onPressed: controller.redactEventsAction, - ), - if (controller.selectedEvents.length == 1) - PopupMenuButton<_EventContextAction>( - onSelected: (action) { - switch (action) { - case _EventContextAction.info: - controller.showEventInfo(); - controller.clearSelectedEvents(); - break; - case _EventContextAction.report: - controller.reportEventAction(); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: _EventContextAction.info, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.info_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), - ], - ), - ), - if (controller.selectedEvents.single.status.isSent) - PopupMenuItem( - value: _EventContextAction.report, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.shield_outlined, - color: Colors.red, - ), - const SizedBox(width: 12), - Text(L10n.of(context)!.reportMessage), - ], - ), - ), - ], - ), - ]; - } else if (!controller.room.isArchived) { - return [ - if (Matrix.of(context).voipPlugin != null && - controller.room.isDirectChat) - IconButton( - onPressed: controller.onPhoneButtonTap, - icon: const Icon(Icons.call_outlined), - tooltip: L10n.of(context)!.placeCall, - ), - EncryptionButton(controller.room), - ChatSettingsPopupMenu(controller.room, true), - ]; - } - return []; - } - @override Widget build(BuildContext context) { if (controller.room.membership == Membership.invite) { @@ -133,240 +21,24 @@ class ChatView extends StatelessWidget { future: () => controller.room.join(), ); } - final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; - final scrollUpBannerEventId = controller.scrollUpBannerEventId; - return PopScope( - canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker, - onPopInvoked: (pop) async { - if (pop) return; - if (controller.selectedEvents.isNotEmpty) { - controller.clearSelectedEvents(); - } else if (controller.showEmojiPicker) { - controller.emojiPickerAction(); - } - }, - child: GestureDetector( - onTapDown: (_) => controller.setReadMarker(), - behavior: HitTestBehavior.opaque, - child: StreamBuilder( - stream: controller.room.onUpdate.stream - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) => FutureBuilder( - future: controller.loadTimelineFuture, - builder: (BuildContext context, snapshot) { - return Scaffold( - appBar: AppBar( - actionsIconTheme: IconThemeData( - color: controller.selectedEvents.isEmpty - ? null - : Theme.of(context).colorScheme.primary, - ), - leading: controller.selectMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ) - : UnreadRoomsBadge( - filter: (r) => r.id != controller.roomId, - badgePosition: BadgePosition.topEnd(end: 8, top: 4), - child: const Center(child: BackButton()), - ), - titleSpacing: 0, - title: ChatAppBarTitle(controller), - actions: _appBarActions(context), - ), - floatingActionButton: controller.showScrollDownButton && - controller.selectedEvents.isEmpty - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - heroTag: null, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), - ), - ) - : null, - body: DropTarget( - onDragDone: controller.onDragDone, - onDragEntered: controller.onDragEntered, - onDragExited: controller.onDragExited, - child: Stack( - children: [ - if (Matrix.of(context).wallpaper != null) - Image.file( - Matrix.of(context).wallpaper!, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - ), - SafeArea( - child: Column( - children: [ - TombstoneDisplay(controller), - if (scrollUpBannerEventId != null) - Material( - color: Theme.of(context) - .colorScheme - .surfaceVariant, - shape: Border( - bottom: BorderSide( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), - child: ListTile( - leading: IconButton( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - icon: const Icon(Icons.close), - tooltip: L10n.of(context)!.close, - onPressed: () { - controller.discardScrollUpBannerEventId(); - controller.setReadMarker(); - }, - ), - title: Text( - L10n.of(context)!.jumpToLastReadMessage, - ), - contentPadding: - const EdgeInsets.only(left: 8), - trailing: TextButton( - onPressed: () { - controller.scrollToEventId( - scrollUpBannerEventId, - ); - controller.discardScrollUpBannerEventId(); - }, - child: Text(L10n.of(context)!.jump), - ), - ), - ), - PinnedEvents(controller), - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: - CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ); - } + return ChatViewScaffold( + controller, + body: ChatViewFrame( + controller, + contentBuilder: (context) { + if (controller.timeline == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } - return ChatEventList( - controller: controller, - ); - }, - ), - ), - ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: - Radius.circular(AppConfig.borderRadius), - bottomRight: - Radius.circular(AppConfig.borderRadius), - ), - elevation: 4, - shadowColor: Colors.black.withAlpha(64), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.white - : Colors.black, - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context)!.reopenChat, - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), - ), - ), - ], - ), - ), - if (controller.dragging) - Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), - ), - ], - ), - ), - ); - }, - ), - ), + return ChatEventList( + controller: controller, + ); + }, ), ); } diff --git a/lib/pages/chat/chat_view_frame.dart b/lib/pages/chat/chat_view_frame.dart new file mode 100644 index 000000000..24273df89 --- /dev/null +++ b/lib/pages/chat/chat_view_frame.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; + +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/pinned_events.dart'; +import 'package:fluffychat/pages/chat/reactions_picker.dart'; +import 'package:fluffychat/pages/chat/reply_display.dart'; +import 'package:fluffychat/pages/chat/tombstone_display.dart'; +import 'package:fluffychat/widgets/connection_status_header.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'chat_emoji_picker.dart'; +import 'chat_input_row.dart'; + +class ChatViewFrame extends StatelessWidget { + final ChatController controller; + final WidgetBuilder contentBuilder; + + const ChatViewFrame(this.controller, + {super.key, required this.contentBuilder}); + + @override + Widget build(BuildContext context) { + final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; + final scrollUpBannerEventId = controller.scrollUpBannerEventId; + + return DropTarget( + onDragDone: controller.onDragDone, + onDragEntered: controller.onDragEntered, + onDragExited: controller.onDragExited, + child: Stack( + children: [ + if (Matrix.of(context).wallpaper != null) + Image.file( + Matrix.of(context).wallpaper!, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ), + SafeArea( + child: Column( + children: [ + TombstoneDisplay(controller), + if (scrollUpBannerEventId != null) + Material( + color: Theme.of(context).colorScheme.surfaceVariant, + shape: Border( + bottom: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, + ), + ), + child: ListTile( + leading: IconButton( + color: Theme.of(context).colorScheme.onSurfaceVariant, + icon: const Icon(Icons.close), + tooltip: L10n.of(context)!.close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); + }, + ), + title: Text( + L10n.of(context)!.jumpToLastReadMessage, + ), + contentPadding: const EdgeInsets.only(left: 8), + trailing: TextButton( + onPressed: () { + controller.scrollToEventId( + scrollUpBannerEventId, + ); + controller.discardScrollUpBannerEventId(); + }, + child: Text(L10n.of(context)!.jump), + ), + ), + ), + PinnedEvents(controller), + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: contentBuilder.call, + ), + ), + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + elevation: 4, + shadowColor: Colors.black.withAlpha(64), + clipBehavior: Clip.hardEdge, + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.black, + child: controller.room.isAbandonedDMRoom == true + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all(16), + foregroundColor: + Theme.of(context).colorScheme.error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: controller.recreateChat, + label: Text( + L10n.of(context)!.reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), + ), + ), + ], + ), + ), + if (controller.dragging) + Container( + color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9), + alignment: Alignment.center, + child: const Icon( + Icons.upload_outlined, + size: 100, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/chat_view_scaffold.dart b/lib/pages/chat/chat_view_scaffold.dart new file mode 100644 index 000000000..a766cda5e --- /dev/null +++ b/lib/pages/chat/chat_view_scaffold.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; + +import 'package:badges/badges.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; +import 'package:fluffychat/pages/chat/encryption_button.dart'; +import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/unread_rooms_badge.dart'; +import '../../utils/stream_extension.dart'; + +enum _EventContextAction { info, report } + +class ChatViewScaffold extends StatelessWidget { + final ChatController controller; + final Widget body; + + const ChatViewScaffold(this.controller, {super.key, required this.body}); + + List _appBarActions(BuildContext context) { + if (controller.selectMode) { + return [ + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context)!.edit, + onPressed: controller.editSelectedEventAction, + ), + IconButton( + icon: const Icon(Icons.copy_outlined), + tooltip: L10n.of(context)!.copy, + onPressed: controller.copyEventsAction, + ), + if (controller.canSaveSelectedEvent) + // Use builder context to correctly position the share dialog on iPad + Builder( + builder: (context) => IconButton( + icon: Icon(Icons.adaptive.share), + tooltip: L10n.of(context)!.share, + onPressed: () => controller.saveSelectedEvent(context), + ), + ), + if (controller.canPinSelectedEvents) + IconButton( + icon: const Icon(Icons.push_pin_outlined), + onPressed: controller.pinEvent, + tooltip: L10n.of(context)!.pinMessage, + ), + if (controller.canRedactSelectedEvents) + IconButton( + icon: const Icon(Icons.delete_outlined), + tooltip: L10n.of(context)!.redactMessage, + onPressed: controller.redactEventsAction, + ), + if (controller.selectedEvents.length == 1) + PopupMenuButton<_EventContextAction>( + onSelected: (action) { + switch (action) { + case _EventContextAction.info: + controller.showEventInfo(); + controller.clearSelectedEvents(); + break; + case _EventContextAction.report: + controller.reportEventAction(); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: _EventContextAction.info, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.messageInfo), + ], + ), + ), + if (controller.selectedEvents.single.status.isSent) + PopupMenuItem( + value: _EventContextAction.report, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.shield_outlined, + color: Colors.red, + ), + const SizedBox(width: 12), + Text(L10n.of(context)!.reportMessage), + ], + ), + ), + ], + ), + ]; + } else if (!controller.room.isArchived) { + return [ + if (Matrix.of(context).voipPlugin != null && + controller.room.isDirectChat) + IconButton( + onPressed: controller.onPhoneButtonTap, + icon: const Icon(Icons.call_outlined), + tooltip: L10n.of(context)!.placeCall, + ), + EncryptionButton(controller.room), + ChatSettingsPopupMenu(controller.room, true), + ]; + } + return []; + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker, + onPopInvoked: (pop) async { + if (pop) return; + if (controller.selectedEvents.isNotEmpty) { + controller.clearSelectedEvents(); + } else if (controller.showEmojiPicker) { + controller.emojiPickerAction(); + } + }, + child: GestureDetector( + onTapDown: (_) => controller.setReadMarker(), + behavior: HitTestBehavior.opaque, + child: StreamBuilder( + stream: controller.room.onUpdate.stream + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) => FutureBuilder( + future: controller.loadTimelineFuture, + builder: (BuildContext context, snapshot) { + return Scaffold( + appBar: AppBar( + actionsIconTheme: IconThemeData( + color: controller.selectedEvents.isEmpty + ? null + : Theme.of(context).colorScheme.primary, + ), + leading: controller.selectMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context)!.close, + color: Theme.of(context).colorScheme.primary, + ) + : UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), + ), + titleSpacing: 0, + title: ChatAppBarTitle(controller), + actions: _appBarActions(context), + ), + floatingActionButton: controller.showScrollDownButton && + controller.selectedEvents.isEmpty + ? Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + heroTag: null, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), + ), + ) + : null, + body: body, + ); + }, + ), + ), + ), + ); + } +}