From 0373d01f1b824c222e31c031375e342a540bdeb9 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 27 Aug 2024 13:41:36 -0400 Subject: [PATCH] simplified positioning of toolbar --- lib/pages/chat/chat.dart | 162 ++-- lib/pages/chat/chat_emoji_picker.dart | 165 ++-- lib/pages/chat/chat_event_list.dart | 1 - lib/pages/chat/chat_view.dart | 236 +++--- lib/pages/chat/events/message.dart | 747 ++++++++++-------- lib/pages/chat/events/message_content.dart | 58 +- lib/pangea/utils/any_state_holder.dart | 3 + lib/pangea/widgets/chat/message_buttons.dart | 16 +- .../chat/message_selection_overlay.dart | 339 ++++---- lib/pangea/widgets/chat/message_toolbar.dart | 254 ++---- .../chat/message_unsubscribed_card.dart | 11 +- lib/pangea/widgets/chat/overlay_footer.dart | 16 +- lib/pangea/widgets/chat/overlay_header.dart | 194 ++--- lib/pangea/widgets/chat/overlay_message.dart | 188 ----- .../toolbar_content_loading_indicator.dart | 8 +- lib/pangea/widgets/igc/pangea_rich_text.dart | 52 +- 16 files changed, 1033 insertions(+), 1417 deletions(-) delete mode 100644 lib/pangea/widgets/chat/overlay_message.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 5f319d48b..08db1a1bf 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -3,14 +3,12 @@ import 'dart:developer'; import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; @@ -18,6 +16,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; @@ -29,8 +28,10 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:fluffychat/pangea/utils/report_message.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; @@ -928,20 +929,17 @@ class ChatController extends State } void copyEventsAction() { - // #Pangea - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# Clipboard.setData(ClipboardData(text: _getSelectedEventString())); setState(() { showEmojiPicker = false; - selectedEvents.clear(); + // #Pangea + // selectedEvents.clear(); + clearSelectedEvents(); + // Pangea# }); } void reportEventAction() async { - // #Pangea - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# final event = selectedEvents.single; // #Pangea clearSelectedEvents(); @@ -1035,9 +1033,6 @@ class ChatController extends State } void redactEventsAction() async { - // #Pangea - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# final reasonInput = selectedEvents.any((event) => event.status.isSent) ? await showTextInputDialog( context: context, @@ -1086,6 +1081,9 @@ class ChatController extends State }, ); } + // #Pangea + clearSelectedEvents(); + // Pangea# setState(() { showEmojiPicker = false; selectedEvents.clear(); @@ -1133,9 +1131,6 @@ class ChatController extends State } void forwardEventsAction() async { - // #Pangea - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# if (selectedEvents.length == 1) { Matrix.of(context).shareContent = selectedEvents.first.getDisplayEvent(timeline!).content; @@ -1169,7 +1164,7 @@ class ChatController extends State selectedEvents.clear(); }); // #Pangea - MatrixState.pAnyState.closeAllOverlays(); + clearSelectedEvents(); // Pangea inputFocus.requestFocus(); } @@ -1283,39 +1278,32 @@ class ChatController extends State } void pickEmojiReactionAction(Iterable allReactionEvents) async { + // #Pangea + MatrixState.pAnyState.closeAllOverlays(); + // Pangea# _allReactionEvents = allReactionEvents; emojiPickerType = EmojiPickerType.reaction; setState(() => showEmojiPicker = true); - // #Pangea - OverlayUtil.showOverlay( - context: context, - child: ChatEmojiPicker(this), - transformTargetId: selectedEvents.first.eventId, - targetAnchor: Alignment.center, - followerAnchor: Alignment.center, - backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), - closePrevOverlay: false, - onDismiss: hideEmojiPicker, - position: OverlayEnum.bottom, - ); - // Pangea# } void sendEmojiAction(String? emoji) async { final events = List.from(selectedEvents); setState(() => selectedEvents.clear()); - // #Pangea - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# for (final event in events) { await room.sendReaction( event.eventId, emoji!, ); } + // #Pangea + clearSelectedEvents(); + // Pangea# } void clearSelectedEvents() => setState(() { + // #Pangea + MatrixState.pAnyState.closeAllOverlays(); + // Pangea# selectedEvents.clear(); showEmojiPicker = false; }); @@ -1552,12 +1540,7 @@ class ChatController extends State bool get isArchived => {Membership.leave, Membership.ban}.contains(room.membership); - void showEventInfo([Event? event]) - // #Pangea - // => - { - MatrixState.pAnyState.closeAllOverlays(); - // Pangea# + void showEventInfo([Event? event]) { (event ?? selectedEvents.single).showInfoDialog(context); // #Pangea clearSelectedEvents(); @@ -1618,80 +1601,51 @@ class ChatController extends State editEvent = null; }); - // #Pangea - final Map _pangeaMessageEvents = {}; - final Map _toolbarDisplayControllers = {}; - - void setPangeaMessageEvent(String eventId) { - final Event? event = timeline!.events.firstWhereOrNull( - (e) => e.eventId == eventId, - ); - if (event == null || timeline == null) return; - _pangeaMessageEvents[eventId] = PangeaMessageEvent( - event: event, - timeline: timeline!, - ownMessage: event.senderId == room.client.userID, - ); - } +// #Pangea + MessageTextSelection textSelection = MessageTextSelection(); - void setToolbarDisplayController( - String eventId, { - Event? nextEvent, - Event? previousEvent, + void showToolbar( + PangeaMessageEvent pangeaMessageEvent, { + MessageMode? mode, }) { - final Event? event = timeline!.events.firstWhereOrNull( - (e) => e.eventId == eventId, - ); - if (event == null || timeline == null) return; - if (_pangeaMessageEvents[eventId] == null) { - setPangeaMessageEvent(eventId); - if (_pangeaMessageEvents[eventId] == null) return; + // Close keyboard, if open + if (inputFocus.hasFocus && PlatformInfos.isMobile) { + inputFocus.unfocus(); + return; } + // Close emoji picker, if open + showEmojiPicker = false; + // Check if the user has set their languages. If not, prompt them to do so. + if (!MatrixState.pangeaController.languageController.languagesSet) { + pLanguageDialog(context, () {}); + return; + } + + Widget? overlayEntry; try { - _toolbarDisplayControllers[eventId] = ToolbarDisplayController( - targetId: event.eventId, - pangeaMessageEvent: _pangeaMessageEvents[eventId]!, - immersionMode: choreographer.immersionMode, + overlayEntry = MessageSelectionOverlay( controller: this, - nextEvent: nextEvent, - previousEvent: previousEvent, - ); - _toolbarDisplayControllers[eventId]!.setToolbar(); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - m: "Failed to set toolbar display controller", - data: { - "eventId": eventId, - "event": event.toJson(), - "pangeaMessageEvent": _pangeaMessageEvents[eventId]?.toString(), - }, + event: pangeaMessageEvent.event, + pangeaMessageEvent: pangeaMessageEvent, + textSelection: textSelection, ); + } catch (err) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: StackTrace.current); + return; } - } - - PangeaMessageEvent? getPangeaMessageEvent(String eventId) { - if (_pangeaMessageEvents[eventId] == null) { - setPangeaMessageEvent(eventId); - } - return _pangeaMessageEvents[eventId]; - } - ToolbarDisplayController? getToolbarDisplayController( - String eventId, { - Event? nextEvent, - Event? previousEvent, - }) { - if (_toolbarDisplayControllers[eventId] == null) { - setToolbarDisplayController( - eventId, - nextEvent: nextEvent, - previousEvent: previousEvent, - ); - } - return _toolbarDisplayControllers[eventId]; + OverlayUtil.showOverlay( + context: context, + child: overlayEntry, + transformTargetId: "", + backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200), + closePrevOverlay: + MatrixState.pangeaController.subscriptionController.isSubscribed, + position: OverlayEnum.centered, + onDismiss: clearSelectedEvents, + ); } // Pangea# diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index f38243b84..e7bf9c744 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -14,90 +14,95 @@ class ChatEmojiPicker extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - return AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - height: controller.showEmojiPicker - ? MediaQuery.of(context).size.height / 2 - : 0, - child: controller.showEmojiPicker - ? DefaultTabController( - length: 2, - child: Column( - children: [ - TabBar( - tabs: [ - Tab(text: L10n.of(context)!.emojis), - Tab(text: L10n.of(context)!.stickers), - ], - ), - Expanded( - child: TabBarView( - children: [ - EmojiPicker( - onEmojiSelected: controller.onEmojiSelected, - onBackspacePressed: controller.emojiPickerBackspace, - config: Config( - emojiViewConfig: EmojiViewConfig( - noRecents: const NoRecent(), - backgroundColor: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - bottomActionBarConfig: const BottomActionBarConfig( - enabled: false, - ), - categoryViewConfig: CategoryViewConfig( - backspaceColor: theme.colorScheme.primary, - iconColor: - theme.colorScheme.primary.withOpacity(0.5), - iconColorSelected: theme.colorScheme.primary, - indicatorColor: theme.colorScheme.primary, - ), - skinToneConfig: SkinToneConfig( - dialogBackgroundColor: Color.lerp( - theme.colorScheme.surface, - theme.colorScheme.primaryContainer, - 0.75, - )!, - indicatorColor: theme.colorScheme.onSurface, + // #Pangea + return Material( + // Pangea# + child: AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + height: controller.showEmojiPicker + ? MediaQuery.of(context).size.height / 2 + : 0, + child: controller.showEmojiPicker + ? DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + tabs: [ + Tab(text: L10n.of(context)!.emojis), + Tab(text: L10n.of(context)!.stickers), + ], + ), + Expanded( + child: TabBarView( + children: [ + EmojiPicker( + onEmojiSelected: controller.onEmojiSelected, + onBackspacePressed: controller.emojiPickerBackspace, + config: Config( + emojiViewConfig: EmojiViewConfig( + noRecents: const NoRecent(), + backgroundColor: Theme.of(context) + .colorScheme + .onInverseSurface, + ), + bottomActionBarConfig: + const BottomActionBarConfig( + enabled: false, + ), + categoryViewConfig: CategoryViewConfig( + backspaceColor: theme.colorScheme.primary, + iconColor: + theme.colorScheme.primary.withOpacity(0.5), + iconColorSelected: theme.colorScheme.primary, + indicatorColor: theme.colorScheme.primary, + ), + skinToneConfig: SkinToneConfig( + dialogBackgroundColor: Color.lerp( + theme.colorScheme.surface, + theme.colorScheme.primaryContainer, + 0.75, + )!, + indicatorColor: theme.colorScheme.onSurface, + ), ), ), - ), - StickerPickerDialog( - room: controller.room, - onSelected: (sticker) { - controller.room.sendEvent( - { - 'body': sticker.body, - 'info': sticker.info ?? {}, - 'url': sticker.url.toString(), - }, - type: EventTypes.Sticker, - ); - controller.hideEmojiPicker(); - }, - ), - ], + StickerPickerDialog( + room: controller.room, + onSelected: (sticker) { + controller.room.sendEvent( + { + 'body': sticker.body, + 'info': sticker.info ?? {}, + 'url': sticker.url.toString(), + }, + type: EventTypes.Sticker, + ); + controller.hideEmojiPicker(); + }, + ), + ], + ), ), - ), - // #Pangea - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: FloatingActionButton( - onPressed: controller.hideEmojiPicker, - shape: const CircleBorder(), - mini: true, - child: const Icon(Icons.close), + // #Pangea + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: FloatingActionButton( + onPressed: controller.hideEmojiPicker, + shape: const CircleBorder(), + mini: true, + child: const Icon(Icons.close), + ), ), - ), - // Pangea# - ], - ), - ) - : null, + // Pangea# + ], + ), + ) + : null, + ), ); } } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 4813823c1..ccb8bf471 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -188,7 +188,6 @@ class ChatEventList extends StatelessWidget { longPressSelect: controller.selectedEvents.isNotEmpty, // #Pangea immersionMode: controller.choreographer.immersionMode, - definitions: controller.choreographer.definitionsEnabled, controller: controller, // Pangea# selected: controller.selectedEvents diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 876db6e68..737b29534 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/chat_input_row.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/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; @@ -36,100 +35,98 @@ class ChatView extends StatelessWidget { 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, - ), - // #Pangea - if (controller.selectedEvents.length == 1 && - controller.selectedEvents.single.messageType == MessageTypes.Text) - // Pangea# - 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), - ], - ), - ), - ], - ), - ]; - // #Pangea - } else { - return [ - RoundTimer(controller: controller), - const SizedBox( - width: 10, - ), - ChatSettingsPopupMenu( - controller.room, - (!controller.room.isDirectChat && !controller.room.isArchived), - ), - ]; - } + // #Pangea + // if (controller.selectMode) { + // return [ + // if (controller.canEditSelectedEvents) + // IconButton( + // icon: const Icon(Icons.edit_outlined), + // tooltip: L10n.of(context)!.edit, + // onPressed: controller.editSelectedEventAction, + // ), + // // #Pangea + // if (controller.selectedEvents.length == 1 && + // controller.selectedEvents.single.messageType == MessageTypes.Text) + // // Pangea# + // 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), + // ], + // ), + // ), + // ], + // ), + // ]; + return [ + RoundTimer(controller: controller), + const SizedBox( + width: 10, + ), + ChatSettingsPopupMenu( + controller.room, + (!controller.room.isDirectChat && !controller.room.isArchived), + ), + ]; // else if (!controller.room.isArchived) { // return [ // if (Matrix.of(context).voipPlugin != null && @@ -196,28 +193,34 @@ class ChatView extends StatelessWidget { } return Scaffold( appBar: AppBar( - actionsIconTheme: IconThemeData( - color: controller.selectedEvents.isEmpty - ? null - : Theme.of(context).colorScheme.primary, + actionsIconTheme: const IconThemeData( + // #Pangea + // color: controller.selectedEvents.isEmpty + // ? null + // : Theme.of(context).colorScheme.primary, + // Pangea# + ), + leading: + // #Pangea + // controller.selectMode + // ? IconButton( + // icon: const Icon(Icons.close), + // onPressed: controller.clearSelectedEvents, + // tooltip: L10n.of(context)!.close, + // color: Theme.of(context).colorScheme.primary, + // ) + // : + // Pangea# + UnreadRoomsBadge( + filter: (r) => + r.id != controller.roomId + // #Pangea + && + !r.isAnalyticsRoom, + // Pangea# + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), ), - 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 - // #Pangea - && - !r.isAnalyticsRoom, - // Pangea# - badgePosition: BadgePosition.topEnd(end: 8, top: 4), - child: const Center(child: BackButton()), - ), titleSpacing: 0, title: ChatAppBarTitle(controller), actions: _appBarActions(context), @@ -501,7 +504,6 @@ class ChatView extends StatelessWidget { ITBar( choreographer: controller.choreographer, ), - ReactionsPicker(controller), ReplyDisplay(controller), ChatInputRow(controller), ChatEmojiPicker(controller), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 280ea3082..cedd9d3c8 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -2,13 +2,14 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; @@ -38,8 +39,8 @@ class Message extends StatelessWidget { // #Pangea // final void Function(Event) onSelect; final bool immersionMode; - final bool definitions; final ChatController controller; + final bool isOverlay; // Pangea# final Color? avatarPresenceBackgroundColor; @@ -64,21 +65,33 @@ class Message extends StatelessWidget { this.avatarPresenceBackgroundColor, // #Pangea required this.immersionMode, - required this.definitions, required this.controller, + this.isOverlay = false, // Pangea# super.key, }); // #Pangea - PangeaMessageEvent? get pangeaMessageEvent => - controller.getPangeaMessageEvent(event.eventId); + void showToolbar(PangeaMessageEvent? pangeaMessageEvent) { + if (pangeaMessageEvent != null && !isOverlay) { + HapticFeedback.mediumImpact(); + controller.showToolbar(pangeaMessageEvent); + } + } // Pangea# @override Widget build(BuildContext context) { // #Pangea debugPrint('Message.build()'); + PangeaMessageEvent? pangeaMessageEvent; + if (event.type == EventTypes.Message) { + pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: event.senderId == Matrix.of(context).client.userID, + ); + } WidgetsBinding.instance.addPostFrameCallback((_) { if (controller.pangeaEditingEvent?.eventId == event.eventId) { pangeaMessageEvent?.updateLatestEdit(); @@ -109,8 +122,13 @@ class Message extends StatelessWidget { // ignore: deprecated_member_use var color = Theme.of(context).colorScheme.surfaceVariant; final displayTime = event.type == EventTypes.RoomCreate || - nextEvent == null || - !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); + nextEvent == null || + !event.originServerTs.sameEnvironment(nextEvent!.originServerTs) + // #Pangea + && + !isOverlay + // Pangea# + ; final nextEventSameSender = nextEvent != null && { EventTypes.Message, @@ -163,370 +181,405 @@ class Message extends StatelessWidget { : Theme.of(context).colorScheme.primary; } - // #Pangea - ToolbarDisplayController? toolbarController; - if (event.type == EventTypes.Message && - !event.redacted && - (event.messageType == MessageTypes.Text || - event.messageType == MessageTypes.Notice || - event.messageType == MessageTypes.Audio)) { - toolbarController = controller.getToolbarDisplayController( - event.eventId, - nextEvent: nextEvent, - previousEvent: previousEvent, - ); - } - // Pangea# - final resetAnimateIn = this.resetAnimateIn; var animateIn = this.animateIn; - final row = StatefulBuilder( - builder: (context, setState) { - if (animateIn && resetAnimateIn != null) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - animateIn = false; - setState(resetAnimateIn); - }); - } - return AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.none, - alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft, - child: animateIn - ? const SizedBox(height: 0, width: double.infinity) - : Stack( - children: [ - Positioned( - top: 0, - bottom: 0, - left: 0, - right: 0, - child: InkWell( - // #Pangea - // onTap: () => onSelect(event), - // onLongPress: () => onSelect(event), - // Pangea# - borderRadius: - BorderRadius.circular(AppConfig.borderRadius / 2), - child: Material( + final row = + // #Pangea + Material( + color: Colors.transparent, + child: + // Pangea# + StatefulBuilder( + builder: (context, setState) { + if (animateIn && resetAnimateIn != null) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + animateIn = false; + setState(resetAnimateIn); + }); + } + return AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.none, + alignment: + ownMessage ? Alignment.bottomRight : Alignment.bottomLeft, + child: animateIn + ? const SizedBox(height: 0, width: double.infinity) + : Stack( + children: [ + Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: InkWell( + // #Pangea + onTap: () => MatrixState.pAnyState.closeOverlay(), + // onTap: () => onSelect(event), + // onLongPress: () => onSelect(event), + // Pangea# borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - color: selected - ? Theme.of(context) - .colorScheme - .secondaryContainer - .withAlpha(100) - : highlightMarker - ? Theme.of(context) - .colorScheme - .tertiaryContainer - .withAlpha(100) - : Colors.transparent, + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + color: selected + ? Theme.of(context) + .colorScheme + .secondaryContainer + .withAlpha(100) + : highlightMarker + ? Theme.of(context) + .colorScheme + .tertiaryContainer + .withAlpha(100) + : Colors.transparent, + ), ), ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: rowMainAxisAlignment, - children: [ - // #Pangea - // if (longPressSelect) - // SizedBox( - // height: 32, - // width: Avatar.defaultSize, - // child: Checkbox.adaptive( - // value: selected, - // shape: const CircleBorder(), - // onChanged: (_) => onSelect(event), - // ), - // ) - // else - // Pangea# - if (nextEventSameSender || ownMessage) - SizedBox( - width: Avatar.defaultSize, - child: Center( - child: SizedBox( - width: 16, - height: 16, - child: event.status == EventStatus.error - ? const Icon(Icons.error, color: Colors.red) - : event.fileSendingStatus != null - ? const CircularProgressIndicator - .adaptive( - strokeWidth: 1, - ) - : null, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: rowMainAxisAlignment, + children: [ + // #Pangea + // if (longPressSelect) + // SizedBox( + // height: 32, + // width: Avatar.defaultSize, + // child: Checkbox.adaptive( + // value: selected, + // shape: const CircleBorder(), + // onChanged: (_) => onSelect(event), + // ), + // ) + // else + // Pangea# + if (nextEventSameSender || + ownMessage + // #Pangea + || + isOverlay + // Pangea# + ) + SizedBox( + width: Avatar.defaultSize, + child: Center( + child: SizedBox( + width: 16, + height: 16, + child: event.status == EventStatus.error + ? const Icon( + Icons.error, + color: Colors.red, + ) + : event.fileSendingStatus != null + ? const CircularProgressIndicator + .adaptive( + strokeWidth: 1, + ) + : null, + ), ), + ) + else + FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + final user = snapshot.data ?? + event.senderFromMemoryOrFallback; + return Avatar( + // mxContent: user.avatarUrl, + // name: user.calcDisplayname(), + // presenceUserId: user.stateKey, + name: "?", + presenceBackgroundColor: + avatarPresenceBackgroundColor, + onTap: () => onAvatarTab(event), + ); + }, ), - ) - else - FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - final user = snapshot.data ?? - event.senderFromMemoryOrFallback; - return Avatar( - // mxContent: user.avatarUrl, - // name: user.calcDisplayname(), - // presenceUserId: user.stateKey, - name: "?", - presenceBackgroundColor: - avatarPresenceBackgroundColor, - onTap: () => onAvatarTab(event), - ); - }, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (!nextEventSameSender) - Padding( - padding: const EdgeInsets.only( - left: 8.0, - bottom: 4, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (!nextEventSameSender + // #Pangea + && + !isOverlay + // Pangea# + ) + Padding( + padding: const EdgeInsets.only( + left: 8.0, + bottom: 4, + ), + child: ownMessage || event.room.isDirectChat + ? const SizedBox(height: 12) + : FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + // final displayname = snapshot.data + // ?.calcDisplayname() ?? + // event.senderFromMemoryOrFallback + // .calcDisplayname(); + const displayname = "?"; + return Text( + displayname, + style: TextStyle( + fontSize: 12, + color: (Theme.of(context) + .brightness == + Brightness.light + ? displayname.color + : displayname + .lightColorText), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, + ), ), - child: ownMessage || event.room.isDirectChat - ? const SizedBox(height: 12) - : FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - // final displayname = snapshot.data - // ?.calcDisplayname() ?? - // event.senderFromMemoryOrFallback - // .calcDisplayname(); - const displayname = "?"; - return Text( - displayname, - style: TextStyle( - fontSize: 12, - color: (Theme.of(context) - .brightness == - Brightness.light - ? displayname.color - : displayname - .lightColorText), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }, + Container( + alignment: alignment, + padding: const EdgeInsets.only(left: 8), + child: GestureDetector( + // #Pangea + onTap: () => + showToolbar(pangeaMessageEvent), + onDoubleTap: () => + showToolbar(pangeaMessageEvent), + onLongPress: () => + showToolbar(pangeaMessageEvent), + // onLongPress: longPressSelect + // ? null + // : () { + // HapticFeedback.heavyImpact(); + // onSelect(event); + // }, + // Pangea# + child: AnimatedOpacity( + opacity: animateIn + ? 0 + : event.redacted || + event.messageType == + MessageTypes + .BadEncrypted || + event.status.isSending + ? 0.5 + : 1, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: Material( + color: noBubble + ? Colors.transparent + : color, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, ), - ), - Container( - alignment: alignment, - padding: const EdgeInsets.only(left: 8), - child: GestureDetector( - // #Pangea - onTap: () => toolbarController?.showToolbar( - context, - ), - onDoubleTap: () => - toolbarController?.showToolbar(context), - // onLongPress: longPressSelect - // ? null - // : () { - // HapticFeedback.heavyImpact(); - // onSelect(event); - // }, - // Pangea# - child: AnimatedOpacity( - opacity: animateIn - ? 0 - : event.redacted || - event.messageType == - MessageTypes.BadEncrypted || - event.status.isSending - ? 0.5 - : 1, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: Material( - color: - noBubble ? Colors.transparent : color, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - ), - // #Pangea - child: CompositedTransformTarget( - link: MatrixState.pAnyState - .layerLinkAndKey(event.eventId) - .link, - child: Container( - key: MatrixState.pAnyState - .layerLinkAndKey(event.eventId) - .key, - // Pangea# - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + // #Pangea + child: CompositedTransformTarget( + link: isOverlay + ? LayerLinkAndKey('overlay_msg') + .link + : MatrixState.pAnyState + .layerLinkAndKey( + event.eventId, + ) + .link, + child: Container( + key: isOverlay + ? LayerLinkAndKey('overlay_msg') + .key + : MatrixState.pAnyState + .layerLinkAndKey( + event.eventId, + ) + .key, + // Pangea# + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + AppConfig.borderRadius, + ), ), - ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 1.5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (event.relationshipType == - RelationshipTypes.reply) - FutureBuilder( - future: event - .getReplyEvent(timeline), - builder: ( - BuildContext context, - snapshot, - ) { - final replyEvent = snapshot - .hasData - ? snapshot.data! - : Event( - eventId: event - .relationshipEventId!, - content: { - 'msgtype': - 'm.text', - 'body': '...', - }, - senderId: - event.senderId, - type: - 'm.room.message', - room: event.room, - status: EventStatus - .sent, - originServerTs: - DateTime.now(), - ); - return Padding( - padding: - const EdgeInsets.only( - bottom: 4.0, - ), - child: InkWell( - borderRadius: - ReplyContent - .borderRadius, - onTap: () => - scrollToEventId( - replyEvent.eventId, + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * + 1.5, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (event.relationshipType == + RelationshipTypes.reply) + FutureBuilder( + future: event.getReplyEvent( + timeline, + ), + builder: ( + BuildContext context, + snapshot, + ) { + final replyEvent = + snapshot.hasData + ? snapshot.data! + : Event( + eventId: event + .relationshipEventId!, + content: { + 'msgtype': + 'm.text', + 'body': + '...', + }, + senderId: event + .senderId, + type: + 'm.room.message', + room: event + .room, + status: + EventStatus + .sent, + originServerTs: + DateTime + .now(), + ); + return Padding( + padding: + const EdgeInsets + .only( + bottom: 4.0, ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: - ownMessage, - timeline: timeline, + child: InkWell( + borderRadius: + ReplyContent + .borderRadius, + onTap: () => + scrollToEventId( + replyEvent.eventId, + ), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: + ownMessage, + timeline: + timeline, + ), ), ), - ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - onInfoTab: onInfoTab, - borderRadius: borderRadius, - // #Pangea - selected: selected, - pangeaMessageEvent: - pangeaMessageEvent, - immersionMode: immersionMode, - toolbarController: - toolbarController, - // Pangea# - ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - ) - // #Pangea - || - (pangeaMessageEvent - ?.showUseType ?? - false) - // Pangea# - ) - Padding( - padding: - const EdgeInsets.only( - top: 4.0, + ); + }, ), - child: Row( - mainAxisSize: - MainAxisSize.min, - children: [ - // #Pangea - if (pangeaMessageEvent - ?.showUseType ?? - false) ...[ - pangeaMessageEvent! - .msgUseType - .iconView( - context, - textColor - .withAlpha(164), - ), - const SizedBox( - width: 4, - ), - ], - if (event - .hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) ...[ - // Pangea# - Icon( - Icons.edit_outlined, - color: textColor - .withAlpha(164), - size: 14, - ), - Text( - ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', - style: TextStyle( + MessageContent( + displayEvent, + textColor: textColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + // #Pangea + selected: selected, + pangeaMessageEvent: + pangeaMessageEvent, + immersionMode: immersionMode, + isOverlay: isOverlay, + controller: controller, + // Pangea# + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes + .edit, + ) + // #Pangea + || + (pangeaMessageEvent + ?.showUseType ?? + false) + // Pangea# + ) + Padding( + padding: + const EdgeInsets.only( + top: 4.0, + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + // #Pangea + if (pangeaMessageEvent + ?.showUseType ?? + false) ...[ + pangeaMessageEvent! + .msgUseType + .iconView( + context, + textColor + .withAlpha(164), + ), + const SizedBox( + width: 4, + ), + ], + if (event + .hasAggregatedEvents( + timeline, + RelationshipTypes + .edit, + )) ...[ + // Pangea# + Icon( + Icons.edit_outlined, color: textColor .withAlpha(164), - fontSize: 12, + size: 14, ), - ), + Text( + ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', + style: TextStyle( + color: textColor + .withAlpha( + 164, + ), + fontSize: 12, + ), + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), ), ), ), ), - ), - ], + ], + ), ), - ), - ], - ), - ], - ), - ); - }, + ], + ), + ], + ), + ); + }, + ), ); Widget container; final showReceiptsRow = @@ -544,7 +597,10 @@ class Message extends StatelessWidget { crossAxisAlignment: ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (displayTime || selected) + // #Pangea + // if (displayTime || selected) + if ((displayTime || selected) && !isOverlay) + // Pangea# Padding( padding: displayTime ? const EdgeInsets.symmetric(vertical: 8.0) @@ -595,7 +651,8 @@ class Message extends StatelessWidget { children: [ if (pangeaMessageEvent?.showMessageButtons ?? false) MessageButtons( - toolbarController: toolbarController, + controller: controller, + pangeaMessageEvent: pangeaMessageEvent!, ), MessageReactions(event, timeline), ], diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 6431f8efb..6a3b2078c 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,16 +1,15 @@ import 'dart:math'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; -import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; @@ -37,8 +36,8 @@ class MessageContent extends StatelessWidget { //here rather than passing the choreographer? pangea rich text, a widget //further down in the chain is also using pangeaController so its not constant final bool immersionMode; - final ToolbarDisplayController? toolbarController; final bool isOverlay; + final ChatController controller; // Pangea# const MessageContent( @@ -50,8 +49,8 @@ class MessageContent extends StatelessWidget { required this.selected, this.pangeaMessageEvent, required this.immersionMode, - required this.toolbarController, this.isOverlay = false, + required this.controller, // Pangea# required this.borderRadius, }); @@ -306,45 +305,34 @@ class MessageContent extends StatelessWidget { style: messageTextStyle, pangeaMessageEvent: pangeaMessageEvent!, immersionMode: immersionMode, - toolbarController: toolbarController, + isOverlay: isOverlay, + controller: controller, ), ); - } else if (pangeaMessageEvent != null) { - toolbarController?.toolbar?.textSelection.setMessageText( - (event.getDisplayEvent(pangeaMessageEvent!.timeline).body), + } + + if (isOverlay) { + controller.textSelection.setMessageText( + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ), ); } return SelectableLinkify( onSelectionChanged: (selection, cause) { - if (cause == SelectionChangedCause.longPress && - toolbarController != null && - pangeaMessageEvent != null && - !(toolbarController!.highlighted) && - !selected) { - return; + if (isOverlay) { + controller.textSelection.onTextSelection(selection); } - toolbarController?.toolbar?.textSelection - .onTextSelection(selection); }, - onTap: () => toolbarController?.showToolbar(context), - contextMenuBuilder: (context, state) => - (toolbarController?.highlighted ?? false) - ? const SizedBox.shrink() - : MessageContextMenu.contextMenuOverride( - context: context, - textSelection: state, - onDefine: () => toolbarController?.showToolbar( - context, - mode: MessageMode.definition, - ), - onListen: () => toolbarController?.showToolbar( - context, - mode: MessageMode.textToSpeech, - ), - ), - enableInteractiveSelection: - toolbarController?.highlighted ?? false, + onTap: () { + if (pangeaMessageEvent != null && !isOverlay) { + HapticFeedback.mediumImpact(); + controller.showToolbar(pangeaMessageEvent!); + } + }, + enableInteractiveSelection: isOverlay, // Pangea# text: event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)!), diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index d7f0b073e..00800a74b 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -83,6 +83,9 @@ class PangeaAnyState { // String chatViewTargetKey(String? roomId) => "chatViewKey$roomId"; // LayerLinkAndKey chatViewLinkAndKey(String? roomId) => // layerLinkAndKey(chatViewTargetKey(roomId)); + + RenderBox? getRenderBox(String key) => + layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?; } class LayerLinkAndKey { diff --git a/lib/pangea/widgets/chat/message_buttons.dart b/lib/pangea/widgets/chat/message_buttons.dart index f7748675f..43dbfc95a 100644 --- a/lib/pangea/widgets/chat/message_buttons.dart +++ b/lib/pangea/widgets/chat/message_buttons.dart @@ -1,27 +1,27 @@ +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:flutter/material.dart'; class MessageButtons extends StatelessWidget { - final ToolbarDisplayController? toolbarController; + final ChatController controller; + final PangeaMessageEvent pangeaMessageEvent; const MessageButtons({ super.key, - this.toolbarController, + required this.controller, + required this.pangeaMessageEvent, }); void showActivity(BuildContext context) { - toolbarController?.showToolbar( - context, + controller.showToolbar( + pangeaMessageEvent, mode: MessageMode.practiceActivity, ); } @override Widget build(BuildContext context) { - if (toolbarController == null) { - return const SizedBox.shrink(); - } return Padding( padding: const EdgeInsets.only(right: 8.0), child: Row( diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 9fcc4f89c..6ef6cb39b 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -1,195 +1,220 @@ +import 'dart:async'; + import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/events/message.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/utils/any_state_holder.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart'; -import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; -class MessageSelectionOverlay extends StatelessWidget { +class MessageSelectionOverlay extends StatefulWidget { final ChatController controller; - final ToolbarDisplayController toolbarController; - final Function closeToolbar; - final Widget toolbar; + final Event event; final PangeaMessageEvent pangeaMessageEvent; - final bool ownMessage; - final bool immersionMode; - final String targetId; + final MessageMode? initialMode; + final MessageTextSelection textSelection; const MessageSelectionOverlay({ required this.controller, - required this.closeToolbar, - required this.toolbar, + required this.event, required this.pangeaMessageEvent, - required this.immersionMode, - required this.ownMessage, - required this.targetId, - required this.toolbarController, + required this.textSelection, + this.initialMode, super.key, }); @override - Widget build(BuildContext context) { - final LayerLinkAndKey layerLinkAndKey = - MatrixState.pAnyState.layerLinkAndKey(targetId); - final targetRenderBox = - layerLinkAndKey.key.currentContext?.findRenderObject(); - - double center = 290; - double? left; - double? right; - bool showDown = false; - final double footerSize = PlatformInfos.isMobile - ? PlatformInfos.isIOS - ? 128 - : 108 - : 143; - final double headerSize = PlatformInfos.isMobile - ? PlatformInfos.isIOS - ? 121 - : 84 - : 77; - final double stackSize = - MediaQuery.of(context).size.height - footerSize - headerSize; + MessageSelectionOverlayState createState() => MessageSelectionOverlayState(); +} + +class MessageSelectionOverlayState extends State { + double overlayBottomOffset = -1; + double adjustedOverlayBottomOffset = -1; + Size? messageSize; + Offset? messageOffset; + + final StreamController _completeAnimationStream = + StreamController.broadcast(); + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // position the overlay directly over the underlying message + setOverlayBottomOffset(); + // wait for the toolbar to animate to full height + _completeAnimationStream.stream.first.then((_) { + if (toolbarHeight == null || + messageSize == null || + messageOffset == null) { + return; + } + + // Once the toolbar has fully expanded, adjust + // the overlay's position if there's an overflow + final overlayTopOffset = messageOffset!.dy - toolbarHeight!; + + final bool hasHeaderOverflow = overlayTopOffset < headerHeight; + final bool hasFooterOverflow = overlayBottomOffset < footerHeight; + + if (hasHeaderOverflow) { + final overlayHeight = toolbarHeight! + messageSize!.height; + adjustedOverlayBottomOffset = screenHeight - + overlayHeight - + footerHeight - + MediaQuery.of(context).padding.bottom; + } else if (hasFooterOverflow) { + adjustedOverlayBottomOffset = footerHeight; + } + + setState(() {}); + }); + } + + @override + void dispose() { + _completeAnimationStream.close(); + super.dispose(); + } + + void setOverlayBottomOffset() { + // Try to get the offset and size of the original message bubble. + // If it fails, return an empty SizedBox. For instance, this can fail if + // you change the screen size while the overlay is open. try { - if (targetRenderBox != null) { - final Size transformTargetSize = (targetRenderBox as RenderBox).size; - final Offset targetOffset = - (targetRenderBox).localToGlobal(Offset.zero); - if (ownMessage) { - right = MediaQuery.of(context).size.width - - targetOffset.dx - - transformTargetSize.width; - } else { - left = - targetOffset.dx - (FluffyThemes.isColumnMode(context) ? 425 : 1); - } - - showDown = targetOffset.dy + transformTargetSize.height / 2 <= - headerSize + stackSize / 2; - - center = targetOffset.dy - - headerSize + - (showDown ? transformTargetSize.height + 3 : (-3)); - // If top of selected message extends below header - if (targetOffset.dy <= headerSize) { - center = transformTargetSize.height + 3; - showDown = true; - } - // If bottom of selected message extends below footer - else if (targetOffset.dy + transformTargetSize.height >= - headerSize + stackSize) { - center = stackSize - transformTargetSize.height - 3; - } - final double midpoint = headerSize + stackSize / 2; - // If message is too long, - // use default location to make full use of screen - if (transformTargetSize.height >= stackSize / 2 - 30) { - center = stackSize / 2 + (showDown ? -30 : 30); - } - // If message is not too long, but too close - // to center of screen, scroll closer to edges - else if (targetOffset.dy + transformTargetSize.height > midpoint - 30 && - targetOffset.dy < midpoint + 30) { - final double scrollUp = midpoint + 30 - targetOffset.dy; - final double scrollDown = - targetOffset.dy + transformTargetSize.height - (midpoint - 30); - final double minScroll = - controller.scrollController.position.minScrollExtent; - final double maxScroll = - controller.scrollController.position.maxScrollExtent; - final double currentOffset = controller.scrollController.offset; - - // If can scroll up, scroll up - if (currentOffset + scrollUp < maxScroll) { - controller.scrollController.animateTo( - currentOffset + scrollUp, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - ); - showDown = false; - center = stackSize / 2 + 27; - } - - // Else if can scroll down, scroll down - else if (currentOffset - scrollDown > minScroll) { - controller.scrollController.animateTo( - currentOffset - scrollDown, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - ); - showDown = true; - center = stackSize / 2 - 27; - } - - // Neither scrolling works; leave message as-is, - // and use centered toolbar location - else { - center = stackSize / 2 + (showDown ? -30 : 30); - } - } + final messageRenderBox = MatrixState.pAnyState.getRenderBox( + widget.event.eventId, + ); + if (messageRenderBox != null && messageRenderBox.hasSize) { + messageSize = messageRenderBox.size; + messageOffset = messageRenderBox.localToGlobal(Offset.zero); + final messageTopOffset = messageOffset!.dy; + overlayBottomOffset = + screenHeight - messageTopOffset - messageSize!.height; } } catch (err) { - controller.showEmojiPicker = false; - controller.selectedEvents.clear(); - MatrixState.pAnyState.closeAllOverlays(); - ErrorHandler.logError(e: err, s: StackTrace.current); - // throw L10n.of(context)!.toolbarError; - return const SizedBox(); + overlayBottomOffset = adjustedOverlayBottomOffset = -1; + } finally { + setState(() {}); + } + } + + // height of the reply/forward bar + the reaction picker + contextual padding + double get footerHeight => + 48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0); + + double get headerHeight => + (Theme.of(context).appBarTheme.toolbarHeight ?? 56) + + MediaQuery.of(context).padding.top; + + double get screenHeight => MediaQuery.of(context).size.height; + + double? get toolbarHeight { + try { + final toolbarRenderBox = MatrixState.pAnyState.getRenderBox( + '${widget.pangeaMessageEvent.eventId}-toolbar', + ); + + return toolbarRenderBox?.size.height; + } catch (e) { + return null; + } + } + + @override + Widget build(BuildContext context) { + if (overlayBottomOffset == -1) { + return const SizedBox.shrink(); } - final Widget overlayMessage = OverlayMessage( - pangeaMessageEvent.event, - timeline: pangeaMessageEvent.timeline, - immersionMode: immersionMode, - ownMessage: pangeaMessageEvent.ownMessage, - toolbarController: toolbarController, - width: 290, - showDown: showDown, + final overlayMessage = ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, + ), + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: widget.pangeaMessageEvent.ownMessage + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: widget.pangeaMessageEvent.ownMessage + ? 0 + : Avatar.defaultSize + 16, + right: widget.pangeaMessageEvent.ownMessage ? 8 : 0, + ), + child: MessageToolbar( + pangeaMessageEvent: widget.pangeaMessageEvent, + controller: widget.controller, + textSelection: widget.textSelection, + completeAnimationStream: _completeAnimationStream, + initialMode: widget.initialMode, + ), + ), + ], + ), + Message( + widget.event, + onSwipe: () => {}, + onInfoTab: (_) => {}, + onAvatarTab: (_) => {}, + scrollToEventId: (_) => {}, + immersionMode: widget.controller.choreographer.immersionMode, + controller: widget.controller, + timeline: widget.controller.timeline!, + isOverlay: true, + animateIn: false, + ), + ], + ), + ), ); return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: - ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + child: Stack( children: [ - OverlayHeader( - controller: controller, - closeToolbar: closeToolbar, - ), - SizedBox( - height: PlatformInfos.isAndroid ? 3 : 6, + AnimatedPositioned( + duration: FluffyThemes.animationDuration, + left: 0, + right: 0, + bottom: adjustedOverlayBottomOffset == -1 + ? overlayBottomOffset + : adjustedOverlayBottomOffset, + child: Align( + alignment: Alignment.center, + child: overlayMessage, + ), ), - Flexible( - child: Stack( + Align( + alignment: Alignment.bottomCenter, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Positioned( - left: left, - right: right, - bottom: stackSize - center + 3, - child: showDown ? overlayMessage : toolbar, - ), - Positioned( - left: left, - right: right, - top: center + 3, - child: showDown ? toolbar : overlayMessage, - ), + OverlayFooter(controller: widget.controller), ], ), ), - SizedBox( - height: PlatformInfos.isAndroid ? 3 : 6, + Material( + child: OverlayHeader(controller: widget.controller), ), - OverlayFooter(controller: controller), ], ), ); diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 238d6b36e..943269bdb 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -1,158 +1,35 @@ import 'dart:async'; -import 'dart:developer'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; -import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -class ToolbarDisplayController { - final PangeaMessageEvent pangeaMessageEvent; - final String targetId; - final bool immersionMode; - final ChatController controller; - final FocusNode focusNode = FocusNode(); - Event? nextEvent; - Event? previousEvent; - - MessageToolbar? toolbar; - String? overlayId; - double? messageWidth; - - final toolbarModeStream = StreamController.broadcast(); - - ToolbarDisplayController({ - required this.pangeaMessageEvent, - required this.targetId, - required this.immersionMode, - required this.controller, - this.nextEvent, - this.previousEvent, - }); - - void closeToolbar() { - controller.clearSelectedEvents(); - MatrixState.pAnyState.closeAllOverlays(); - } - - void setToolbar() { - toolbar ??= MessageToolbar( - textSelection: MessageTextSelection(), - room: pangeaMessageEvent.room, - toolbarModeStream: toolbarModeStream, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - controller: controller, - ); - } - - void showToolbar(BuildContext context, {MessageMode? mode}) { - // Close keyboard, if open - if (controller.inputFocus.hasFocus) { - controller.inputFocus.unfocus(); - return; - } - // Close emoji picker, if open - controller.showEmojiPicker = false; - if (highlighted) return; - if (!MatrixState.pangeaController.languageController.languagesSet) { - pLanguageDialog(context, () {}); - return; - } - focusNode.requestFocus(); - - // I'm not sure why I put this here, but it causes the toolbar - // not to open immediately after clicking (user has to scroll or move their cursor) - // so I'm commenting it out for now - // WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Widget? overlayEntry; - if (toolbar == null) return; - try { - overlayEntry = MessageSelectionOverlay( - controller: controller, - closeToolbar: closeToolbar, - toolbar: toolbar!, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - ownMessage: pangeaMessageEvent.ownMessage, - targetId: targetId, - toolbarController: this, - ); - } catch (err) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: StackTrace.current); - return; - } - - OverlayUtil.showOverlay( - context: context, - child: overlayEntry, - transformTargetId: targetId, - targetAnchor: Alignment.center, - followerAnchor: Alignment.center, - backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200), - closePrevOverlay: - MatrixState.pangeaController.subscriptionController.isSubscribed, - position: OverlayEnum.centered, - onDismiss: controller.clearSelectedEvents, - ); - - controller.onSelectMessage(pangeaMessageEvent.event); - - if (MatrixState.pAnyState.entries.isNotEmpty) { - overlayId = MatrixState.pAnyState.entries.last.hashCode.toString(); - } - - if (mode != null) { - Future.delayed( - const Duration(milliseconds: 100), - () => toolbarModeStream.add(mode), - ); - } - } - - bool get highlighted { - if (overlayId == null) return false; - if (MatrixState.pAnyState.entries.isEmpty) { - overlayId = null; - return false; - } - return MatrixState.pAnyState.entries.last.hashCode.toString() == overlayId; - } -} class MessageToolbar extends StatefulWidget { final MessageTextSelection textSelection; - final Room room; final PangeaMessageEvent pangeaMessageEvent; - final StreamController toolbarModeStream; - final bool immersionMode; final ChatController controller; + final MessageMode? initialMode; + + final StreamController completeAnimationStream; const MessageToolbar({ super.key, required this.textSelection, - required this.room, required this.pangeaMessageEvent, - required this.toolbarModeStream, - required this.immersionMode, required this.controller, + required this.completeAnimationStream, + this.initialMode, }); @override @@ -164,7 +41,6 @@ class MessageToolbarState extends State { MessageMode? currentMode; bool updatingMode = false; late StreamSubscription selectionStream; - late StreamSubscription toolbarModeStream; void updateMode(MessageMode newMode) { //Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget. @@ -203,7 +79,7 @@ class MessageToolbarState extends State { toolbarContent = MessageUnsubscribedCard( languageTool: newMode.title(context), mode: newMode, - toolbarModeStream: widget.toolbarModeStream, + controller: this, ); } else { switch (currentMode) { @@ -242,7 +118,7 @@ class MessageToolbarState extends State { debugPrint("show translation"); toolbarContent = MessageTranslationCard( messageEvent: widget.pangeaMessageEvent, - immersionMode: widget.immersionMode, + immersionMode: widget.controller.choreographer.immersionMode, selection: widget.textSelection, ); } @@ -275,7 +151,7 @@ class MessageToolbarState extends State { fullText: widget.textSelection.messageText, fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, hasInfo: true, - room: widget.room, + room: widget.controller.room, ); } @@ -294,20 +170,20 @@ class MessageToolbarState extends State { super.initState(); widget.textSelection.selectedText = null; - toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) { - updateMode(mode); - }); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (widget.pangeaMessageEvent.isAudioMessage) { updateMode(MessageMode.speechToText); return; } - MatrixState.pangeaController.userController.profile.userSettings - .autoPlayMessages - ? updateMode(MessageMode.textToSpeech) - : updateMode(MessageMode.translation); + if (widget.initialMode != null) { + updateMode(widget.initialMode!); + } else { + MatrixState.pangeaController.userController.profile.userSettings + .autoPlayMessages + ? updateMode(MessageMode.textToSpeech) + : updateMode(MessageMode.translation); + } }); Timer? timer; @@ -330,22 +206,37 @@ class MessageToolbarState extends State { @override void dispose() { selectionStream.cancel(); - toolbarModeStream.cancel(); super.dispose(); } @override Widget build(BuildContext context) { - final double maxHeight = (MediaQuery.of(context).size.height - - (PlatformInfos.isWeb - ? 217 - : PlatformInfos.isIOS - ? 262 - : 198)) / - 2 + - 30; + final buttonRow = Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values + .map( + (mode) => mode.isValidMode(widget.pangeaMessageEvent.event) + ? Tooltip( + message: mode.tooltip(context), + child: IconButton( + icon: Icon(mode.icon), + color: mode.iconColor( + widget.pangeaMessageEvent, + currentMode, + context, + ), + onPressed: () => updateMode(mode), + ), + ) + : const SizedBox.shrink(), + ) + .toList(), + ); return Material( + key: MatrixState.pAnyState + .layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar') + .key, type: MaterialType.transparency, child: Container( padding: const EdgeInsets.all(10), @@ -359,63 +250,26 @@ class MessageToolbarState extends State { Radius.circular(25), ), ), - constraints: BoxConstraints( - maxWidth: 290, - minWidth: 290, - maxHeight: maxHeight, - ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Container( - constraints: BoxConstraints( - minWidth: 290, - maxHeight: maxHeight - 72, - ), - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: toolbarContent ?? const SizedBox(), - ), - SizedBox(height: toolbarContent == null ? 0 : 20), - ], + if (toolbarContent != null) + Container( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 16), + constraints: const BoxConstraints( + maxWidth: 275, + minWidth: 275, + maxHeight: 250, + ), + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent, + onEnd: () => widget.completeAnimationStream.add(null), ), ), ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: MessageMode.values.map((mode) { - if ([ - MessageMode.definition, - MessageMode.textToSpeech, - MessageMode.translation, - ].contains(mode) && - widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - if (mode == MessageMode.speechToText && - !widget.pangeaMessageEvent.isAudioMessage) { - return const SizedBox.shrink(); - } - return Tooltip( - message: mode.tooltip(context), - child: IconButton( - icon: Icon(mode.icon), - color: mode.iconColor( - widget.pangeaMessageEvent, - currentMode, - context, - ), - onPressed: () => updateMode(mode), - ), - ); - }).toList(), - ), + buttonRow, ], ), ), diff --git a/lib/pangea/widgets/chat/message_unsubscribed_card.dart b/lib/pangea/widgets/chat/message_unsubscribed_card.dart index 720b25748..d1ff5c343 100644 --- a/lib/pangea/widgets/chat/message_unsubscribed_card.dart +++ b/lib/pangea/widgets/chat/message_unsubscribed_card.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -11,13 +10,13 @@ import '../../enum/message_mode_enum.dart'; class MessageUnsubscribedCard extends StatelessWidget { final String languageTool; final MessageMode mode; - final StreamController toolbarModeStream; + final MessageToolbarState controller; const MessageUnsubscribedCard({ super.key, required this.languageTool, required this.mode, - required this.toolbarModeStream, + required this.controller, }); @override @@ -29,7 +28,7 @@ class MessageUnsubscribedCard extends StatelessWidget { if (inTrialWindow) { MatrixState.pangeaController.subscriptionController .activateNewUserTrial(); - toolbarModeStream.add(mode); + controller.updateMode(mode); } else { MatrixState.pangeaController.subscriptionController .showPaywall(context); @@ -49,7 +48,7 @@ class MessageUnsubscribedCard extends StatelessWidget { child: TextButton( onPressed: onButtonPress, style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( (AppConfig.primaryColor).withOpacity(0.1), ), ), diff --git a/lib/pangea/widgets/chat/overlay_footer.dart b/lib/pangea/widgets/chat/overlay_footer.dart index a15045750..b4c51d07c 100644 --- a/lib/pangea/widgets/chat/overlay_footer.dart +++ b/lib/pangea/widgets/chat/overlay_footer.dart @@ -2,24 +2,23 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; class OverlayFooter extends StatelessWidget { - ChatController controller; + final ChatController controller; - OverlayFooter({ + const OverlayFooter({ required this.controller, super.key, }); @override Widget build(BuildContext context) { - final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 18.0 : 10.0; + final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; return Container( margin: EdgeInsets.only( - bottom: PlatformInfos.isAndroid ? 0 : bottomSheetPadding, + bottom: bottomSheetPadding, left: bottomSheetPadding, right: bottomSheetPadding, ), @@ -42,13 +41,6 @@ class OverlayFooter extends StatelessWidget { ], ), ), - SizedBox( - height: FluffyThemes.isColumnMode(context) - ? 15.0 - : PlatformInfos.isAndroid - ? 0 - : 8.0, - ), ], ), ); diff --git a/lib/pangea/widgets/chat/overlay_header.dart b/lib/pangea/widgets/chat/overlay_header.dart index b9a073125..c8f5b2a62 100644 --- a/lib/pangea/widgets/chat/overlay_header.dart +++ b/lib/pangea/widgets/chat/overlay_header.dart @@ -1,148 +1,92 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; -import 'package:fluffychat/pangea/utils/overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class OverlayHeader extends StatelessWidget { - ChatController controller; - Function closeToolbar; + final ChatController controller; - OverlayHeader({ + const OverlayHeader({ required this.controller, - required this.closeToolbar, super.key, }); @override Widget build(BuildContext context) { - final Event selectedEvent = controller.selectedEvents.single; - - return AppBar( - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - actionsIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.primary, - ), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => closeToolbar(), - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ), - titleSpacing: 0, - title: ChatAppBarTitle(controller), - actions: [ - if (controller.canEditSelectedEvents) - IconButton( - icon: const Icon(Icons.edit_outlined), - tooltip: L10n.of(context)!.edit, - onPressed: controller.editSelectedEventAction, - ), - if (selectedEvent.messageType == MessageTypes.Text) - 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, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppBar( + actionsIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.primary, ), - if (controller.canRedactSelectedEvents) - IconButton( - icon: const Icon(Icons.delete_outlined), - tooltip: L10n.of(context)!.redactMessage, - onPressed: controller.redactEventsAction, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + controller.clearSelectedEvents(); + MatrixState.pAnyState.closeAllOverlays(); + }, + tooltip: L10n.of(context)!.close, + color: Theme.of(context).colorScheme.primary, ), - IconButton( - padding: const EdgeInsets.only(bottom: 6), - icon: Icon( - Icons.more_horiz, - color: Theme.of(context).colorScheme.onSurface, - ), - onPressed: () => showPopup(context), - ), - ], - ); - } - - void showPopup(BuildContext context) { - OverlayUtil.showOverlay( - context: context, - child: SelectionPopup(controller: controller), - transformTargetId: "", - targetAnchor: Alignment.center, - followerAnchor: Alignment.center, - closePrevOverlay: false, - position: OverlayEnum.topRight, - ); - } -} - -class SelectionPopup extends StatelessWidget { - ChatController controller; - - SelectionPopup({ - required this.controller, - super.key, - }); - - @override - Widget build(BuildContext context) { - return Material( - type: MaterialType.transparency, - child: Container( - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: const BorderRadius.all( - Radius.circular(20), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton( - onPressed: controller.showEventInfo, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.info_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), - ], + titleSpacing: 0, + title: ChatAppBarTitle(controller), + actions: [ + if (controller.canEditSelectedEvents) + IconButton( + icon: const Icon(Icons.edit_outlined), + tooltip: L10n.of(context)!.edit, + onPressed: controller.editSelectedEventAction, + ), + if (controller.selectedEvents.length == 1 && + controller.selectedEvents.single.messageType == + MessageTypes.Text) + 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, ), - ), - TextButton( - onPressed: controller.reportEventAction, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.shield_outlined, - color: Colors.red, - ), - const SizedBox(width: 12), - Text(L10n.of(context)!.reportMessage), - ], + if (controller.canRedactSelectedEvents) + IconButton( + icon: const Icon(Icons.delete_outlined), + tooltip: L10n.of(context)!.redactMessage, + onPressed: controller.redactEventsAction, + ), + if (controller.selectedEvents.length == 1) + IconButton( + icon: const Icon(Icons.info_outlined), + tooltip: L10n.of(context)!.messageInfo, + onPressed: () { + controller.showEventInfo(); + controller.clearSelectedEvents(); + }, + ), + if (controller.selectedEvents.length == 1) + IconButton( + icon: const Icon(Icons.shield_outlined), + tooltip: L10n.of(context)!.reportMessage, + onPressed: controller.reportEventAction, ), - ), ], ), - ), + ], ); } } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart deleted file mode 100644 index 3f8869399..000000000 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/events/message_content.dart'; -import 'package:fluffychat/pangea/enum/use_type.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; -import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -import '../../../config/app_config.dart'; - -class OverlayMessage extends StatelessWidget { - final Event event; - final bool selected; - final Timeline timeline; - // final LanguageModel? selectedDisplayLang; - final bool immersionMode; - // final bool definitions; - final bool ownMessage; - final ToolbarDisplayController toolbarController; - final double? width; - final bool showDown; - - const OverlayMessage( - this.event, { - this.selected = false, - required this.timeline, - required this.immersionMode, - required this.ownMessage, - required this.toolbarController, - required this.showDown, - this.width, - super.key, - }); - - @override - Widget build(BuildContext context) { - if (event.type != EventTypes.Message || - event.messageType == EventTypes.KeyVerificationRequest) { - return const SizedBox.shrink(); - } - - var color = Theme.of(context).colorScheme.surfaceContainer; - final isLight = Theme.of(context).brightness == Brightness.light; - var lightness = isLight ? .05 : .2; - final textColor = ownMessage - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurface; - - const hardCorner = Radius.circular(4); - const roundedCorner = Radius.circular(AppConfig.borderRadius); - final borderRadius = BorderRadius.only( - topLeft: !showDown && !ownMessage ? hardCorner : roundedCorner, - topRight: !showDown && ownMessage ? hardCorner : roundedCorner, - bottomLeft: showDown && !ownMessage ? hardCorner : roundedCorner, - bottomRight: showDown && ownMessage ? hardCorner : roundedCorner, - ); - - final noBubble = { - MessageTypes.Video, - MessageTypes.Image, - MessageTypes.Sticker, - }.contains(event.messageType) && - !event.redacted; - final noPadding = { - MessageTypes.File, - MessageTypes.Audio, - }.contains(event.messageType); - - if (ownMessage) { - color = Theme.of(context).colorScheme.primary; - lightness = isLight ? .15 : .85; - } - // Make overlay a little darker/lighter than the message - color = Color.fromARGB( - color.alpha, - isLight || !ownMessage - ? (color.red + lightness * (255 - color.red)).round() - : (color.red * lightness).round(), - isLight || !ownMessage - ? (color.green + lightness * (255 - color.green)).round() - : (color.green * lightness).round(), - isLight || !ownMessage - ? (color.blue + lightness * (255 - color.blue)).round() - : (color.blue * lightness).round(), - ); - - final double maxHeight = (MediaQuery.of(context).size.height - - (PlatformInfos.isWeb - ? 228 - : PlatformInfos.isIOS - ? 258 - : 198)) / - 2 - - 30; - - final pangeaMessageEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: ownMessage, - ); - - return Material( - color: noBubble ? Colors.transparent : color, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: width ?? FluffyThemes.columnWidth * 1.25, - maxHeight: maxHeight, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: MessageContent( - event.getDisplayEvent(timeline), - textColor: textColor, - borderRadius: borderRadius, - selected: selected, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - toolbarController: toolbarController, - isOverlay: true, - ), - ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - ) || - (pangeaMessageEvent.showUseType)) - Padding( - padding: const EdgeInsets.only( - top: 4.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (pangeaMessageEvent.showUseType) ...[ - pangeaMessageEvent.msgUseType.iconView( - context, - textColor.withAlpha(164), - ), - const SizedBox(width: 4), - ], - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) ...[ - Icon( - Icons.edit_outlined, - color: textColor.withAlpha(164), - size: 14, - ), - Text( - ' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}', - style: TextStyle( - color: textColor.withAlpha(164), - fontSize: 12, - ), - ), - ], - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart index 021f4ee44..9edc9971d 100644 --- a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart +++ b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart @@ -7,13 +7,15 @@ class ToolbarContentLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( + return Center( + child: SizedBox( height: 14, width: 14, child: CircularProgressIndicator( strokeWidth: 2.0, color: Theme.of(context).colorScheme.primary, ), - ); + ), + ); } -} \ No newline at end of file +} diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 1ad2e6e08..3ff10d4c7 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -2,31 +2,31 @@ import 'dart:developer'; import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../enum/message_mode_enum.dart'; import '../../models/pangea_match_model.dart'; class PangeaRichText extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final bool immersionMode; - final ToolbarDisplayController? toolbarController; final TextStyle? style; + final bool isOverlay; + final ChatController controller; const PangeaRichText({ super.key, required this.pangeaMessageEvent, required this.immersionMode, - required this.toolbarController, + required this.isOverlay, + required this.controller, this.style, }); @@ -59,12 +59,11 @@ class PangeaRichTextState extends State { void _setTextSpan(String newTextSpan) { try { if (!mounted) return; // Early exit if the widget is no longer in the tree - - widget.toolbarController?.toolbar?.textSelection.setMessageText( - newTextSpan, - ); setState(() { textSpan = newTextSpan; + if (widget.isOverlay) { + widget.controller.textSelection.setMessageText(textSpan); + } }); } catch (error, stackTrace) { ErrorHandler.logError( @@ -137,35 +136,16 @@ class PangeaRichTextState extends State { //TODO - take out of build function of every message final Widget richText = SelectableText.rich( onSelectionChanged: (selection, cause) { - if (cause == SelectionChangedCause.longPress && - !(widget.toolbarController?.highlighted ?? false) && - !(widget.toolbarController?.controller.selectedEvents.any( - (e) => e.eventId == widget.pangeaMessageEvent.eventId, - ) ?? - false)) { - return; + if (widget.isOverlay) { + widget.controller.textSelection.onTextSelection(selection); } - widget.toolbarController?.toolbar?.textSelection - .onTextSelection(selection); }, - onTap: () => widget.toolbarController?.showToolbar(context), - enableInteractiveSelection: - widget.toolbarController?.highlighted ?? false, - contextMenuBuilder: (context, state) => - widget.toolbarController?.highlighted ?? true - ? const SizedBox.shrink() - : MessageContextMenu.contextMenuOverride( - context: context, - textSelection: state, - onDefine: () => widget.toolbarController?.showToolbar( - context, - mode: MessageMode.definition, - ), - onListen: () => widget.toolbarController?.showToolbar( - context, - mode: MessageMode.textToSpeech, - ), - ), + onTap: () { + if (!widget.isOverlay) { + widget.controller.showToolbar(widget.pangeaMessageEvent); + } + }, + enableInteractiveSelection: widget.isOverlay, TextSpan( text: textSpan, style: widget.style,