From 57957eac9e0407f784481546fab21ac0246b96de Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sat, 27 Jan 2024 12:08:19 -0500 Subject: [PATCH] experimenting --- lib/main.dart | 15 +- lib/pages/chat/chat.dart | 3 +- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/events/message.dart | 8 +- lib/pages/chat/events/message_content.dart | 1 + .../controllers/message_data_controller.dart | 11 +- .../extensions/pangea_room_extension.dart | 84 ++-- lib/pangea/utils/any_state_holder.dart | 6 - lib/pangea/utils/toolbar_util.dart | 364 ++++++++++++++---- lib/pangea/widgets/igc/pangea_rich_text.dart | 1 + 10 files changed, 346 insertions(+), 149 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f2123f915..88576b23d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,4 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; @@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/error_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index f31568064..4d1eddcbd 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1277,10 +1277,11 @@ class ChatController extends State } void onSelectMessage(Event event) { -// #Pangea + // #Pangea if (choreographer.itController.isOpen) { return; } + // Pangea# if (!event.redacted) { if (selectedEvents.contains(event)) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index c30de518b..e69b7d074 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -148,7 +148,7 @@ class ChatEventList extends StatelessWidget { scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), // #Pangea - // longPressSelect: controller.selectedEvents.isEmpty, + longPressSelect: controller.selectedEvents.isNotEmpty, selectedDisplayLang: controller.choreographer.messageOptions.selectedDisplayLang, immersionMode: controller.choreographer.immersionMode, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 478103a03..36bdb9fee 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -241,8 +241,14 @@ class Message extends StatelessWidget { alignment: alignment, padding: const EdgeInsets.only(left: 8), child: GestureDetector( + onTap: () => print("got message tap"), + onDoubleTap: () => print("got message double tap"), + onDoubleTapDown: (details) => + print("got message double tap down"), onLongPress: longPressSelect - ? null + ? selected + ? null + : () => print('long press') : () { onSelect(event); // Android usually has a vibration effect on long press: diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 0f6765df2..7a7c0b5bb 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -361,6 +361,7 @@ class MessageContent extends StatelessWidget { cause: cause, context: context, ), + onTap: () => messageToolbar?.onTextTap(context), ); }, ), diff --git a/lib/pangea/controllers/message_data_controller.dart b/lib/pangea/controllers/message_data_controller.dart index 130afcdfe..acaa18657 100644 --- a/lib/pangea/controllers/message_data_controller.dart +++ b/lib/pangea/controllers/message_data_controller.dart @@ -1,17 +1,16 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; import 'package:fluffychat/pangea/repo/tokens_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../constants/pangea_event_types.dart'; import '../enum/use_type.dart'; import '../models/choreo_record.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index dd51a06ce..70d6834ec 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -591,51 +591,51 @@ extension PangeaRoom on Room { required String parentEventId, required String type, }) async { - try { - debugPrint("creating $type child for $parentEventId"); - Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); - if (parentEventId.contains("web")) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "sendPangeaEvent with likely invalid parentEventId $parentEventId", - ), - ); - } - final Map repContent = { - // what is the functionality of m.reference? - "m.relates_to": {"rel_type": type, "event_id": parentEventId}, - type: content, - }; - - final String? newEventId = await sendEvent(repContent, type: type); - - if (newEventId == null) { - debugger(when: kDebugMode); - } - - //PTODO - handle the frequent case of a null newEventId - final Event? newEvent = await getEventById(newEventId!); - - if (newEvent == null) { - debugger(when: kDebugMode); - } - - return newEvent; - } catch (err, stack) { + // try { + debugPrint("creating $type child for $parentEventId"); + Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); + if (parentEventId.contains("web")) { debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "type": type, - "parentEventId": parentEventId, - "content": content, - }, + Sentry.addBreadcrumb( + Breadcrumb( + message: + "sendPangeaEvent with likely invalid parentEventId $parentEventId", + ), ); - return null; } + final Map repContent = { + // what is the functionality of m.reference? + "m.relates_to": {"rel_type": type, "event_id": parentEventId}, + type: content, + }; + + final String? newEventId = await sendEvent(repContent, type: type); + + if (newEventId == null) { + debugger(when: kDebugMode); + } + + //PTODO - handle the frequent case of a null newEventId + final Event? newEvent = await getEventById(newEventId!); + + if (newEvent == null) { + debugger(when: kDebugMode); + } + + return newEvent; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // e: err, + // s: stack, + // data: { + // "type": type, + // "parentEventId": parentEventId, + // "content": content, + // }, + // ); + // return null; + // } } ConstructEvent? _vocabEventLocal(String lemma) { diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index e7ee11451..fd6124420 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,13 +1,7 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../models/widget_measurement.dart'; - class PangeaAnyState { - final Map?> _streams = {}; - final Map> _pastValues = {}; final Map _layerLinkAndKeys = {}; OverlayEntry? overlay; diff --git a/lib/pangea/utils/toolbar_util.dart b/lib/pangea/utils/toolbar_util.dart index 409bcd034..4848269fb 100644 --- a/lib/pangea/utils/toolbar_util.dart +++ b/lib/pangea/utils/toolbar_util.dart @@ -1,94 +1,138 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; enum MessageMode { translation, play, definition, image, spellCheck } -class MessageOverlay { - static void showOverlay(BuildContext context, GlobalKey targetKey) { - final RenderBox renderBox = - targetKey.currentContext?.findRenderObject() as RenderBox; - final Offset offset = renderBox.localToGlobal(Offset.zero); - final Size size = renderBox.size; - final double screenWidth = MediaQuery.of(context).size.width; +class MessageOverlayController { + OverlayEntry? _overlayEntry; + final BuildContext _context; + final GlobalKey _targetKey; + MessageMode? _currentMode; + AnimationController? _animationController; - // Determines the vertical position of the overlay - final bool isBottomRoomAvailable = - MediaQuery.of(context).size.height - (offset.dy + size.height) >= - size.height; - - OverlayEntry overlayEntry; - MessageMode currentMode = MessageMode.translation; - - // Function to build the content based on the selected mode - Widget buildContent() { - switch (currentMode) { - case MessageMode.translation: - return const Text('Translation Mode'); - case MessageMode.play: - return const Text('Play Mode'); - case MessageMode.definition: - return const Text('Definition Mode'); - case MessageMode.image: - return const Text('Image Mode'); - case MessageMode.spellCheck: - return const Text('SpellCheck Mode'); - default: - return const SizedBox.shrink(); // Returns an empty container - } - } - - // Function to show the overlay with an animation - overlayEntry = OverlayEntry( - builder: (context) => AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - left: offset.dx + size.width / 2 - screenWidth / 2, - right: screenWidth - (offset.dx + size.width / 2 + screenWidth / 2), - top: isBottomRoomAvailable ? offset.dy + size.height : null, - bottom: isBottomRoomAvailable - ? null - : MediaQuery.of(context).size.height - offset.dy, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - width: screenWidth, - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Material( - elevation: 4.0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - alignment: WrapAlignment.center, - children: MessageMode.values.map((mode) { - return IconButton( - icon: Icon(_getIconData(mode)), - onPressed: () { - currentMode = mode; - overlayEntry.markNeedsBuild(); - }, - ); - }).toList(), - ), - SizeTransition( - sizeFactor: currentMode != null - ? CurvedAnimation( - parent: Overlay.of(context).animation!, - curve: Curves.fastOutSlowIn) - : const AlwaysStoppedAnimation(0), - axisAlignment: -1.0, - child: buildContent(), - ), - ], - ), - ), - ), - ), + MessageOverlayController(this._context, this._targetKey) { + _animationController = AnimationController( + vsync: Navigator.of(_context), // Using the Navigator's TickerProvider + duration: const Duration(milliseconds: 300), ); - - Overlay.of(context).insert(overlayEntry); } - static IconData _getIconData(MessageMode mode) { + void showOverlay() { + final RenderBox renderBox = + _targetKey.currentContext?.findRenderObject() as RenderBox; + final Offset offset = renderBox.localToGlobal(Offset.zero); + final Size size = renderBox.size; + final double screenWidth = MediaQuery.of(_context).size.width; + + // Determines if there is more room above or below the RenderBox + final bool isBottomRoomAvailable = + MediaQuery.of(_context).size.height - (offset.dy + size.height) >= + size.height; + final double topPosition = isBottomRoomAvailable + ? offset.dy + size.height + : offset.dy - size.height; + + // Ensure the overlay does not overflow the screen horizontally + double leftPosition = offset.dx + size.width / 2 - screenWidth / 2; + leftPosition = leftPosition < 0 ? 0 : leftPosition; + final double rightPosition = + leftPosition + screenWidth > MediaQuery.of(_context).size.width + ? MediaQuery.of(_context).size.width - leftPosition - screenWidth + : leftPosition; + + _overlayEntry = OverlayEntry( + builder: (context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + left: leftPosition, + right: rightPosition, + top: isBottomRoomAvailable ? topPosition : null, + bottom: isBottomRoomAvailable + ? null + : MediaQuery.of(_context).size.height - + topPosition - + size.height, + child: AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300), + child: Material( + elevation: 4.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + return IconButton( + icon: Icon(_getIconData(mode)), + onPressed: () { + setState(() { + _currentMode = mode; + }); + _animationController?.forward(); + }, + ); + }).toList(), + ), + SizeTransition( + sizeFactor: CurvedAnimation( + parent: _animationController!, + curve: Curves.fastOutSlowIn, + ), + axisAlignment: -1.0, + child: _buildModeContent(), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + + Overlay.of(_context).insert(_overlayEntry!); + } + + void hideOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + _animationController?.reverse(); + } + + Widget _buildModeContent() { + switch (_currentMode) { + case MessageMode.translation: + return const Text('Translation Mode'); + case MessageMode.play: + return const Text('Play Mode'); + case MessageMode.definition: + return const Text('Definition Mode'); + case MessageMode.image: + return const Text('Image Mode'); + case MessageMode.spellCheck: + return const Text('SpellCheck Mode'); + default: + return const SizedBox + .shrink(); // Empty container for the default case, meaning no content + } + } + + IconData _getIconData(MessageMode mode) { switch (mode) { case MessageMode.translation: return Icons.g_translate; @@ -101,7 +145,159 @@ class MessageOverlay { case MessageMode.spellCheck: return Icons.spellcheck; default: - return Icons.error; + return Icons.error; // Icon to indicate an error or unsupported mode } } + + void dispose() { + _overlayEntry?.dispose(); + _animationController?.dispose(); + } +} + +class ShowDefintionUtil { + String messageText; + final String langCode; + final String targetId; + final FocusNode focusNode = FocusNode(); + final Room room; + String? textSelection; + bool inCooldown = false; + double? dx; + double? dy; + + ShowDefintionUtil({ + required this.targetId, + required this.room, + required this.langCode, + required this.messageText, + }); + + void onTextSelection({ + required BuildContext context, + TextSelection? selectedText, + SelectedContent? selectedContent, + SelectionChangedCause? cause, + }) { + if ((selectedText == null && selectedContent == null) || + selectedText?.isCollapsed == true) { + clearTextSelection(); + return; + } + textSelection = selectedText != null + ? selectedText.textInside(messageText) + : selectedContent!.plainText; + + if (BrowserContextMenu.enabled && kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + + if (kIsWeb && cause != SelectionChangedCause.tap) { + handleToolbar(context); + } + } + + void clearTextSelection() { + textSelection = null; + if (kIsWeb && !BrowserContextMenu.enabled) { + BrowserContextMenu.enableContextMenu(); + } + } + + void handleToolbar(BuildContext context) async { + if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return; + inCooldown = true; + Timer(const Duration(milliseconds: 750), () => inCooldown = false); + await Future.delayed(const Duration(milliseconds: 750)); + showToolbar(context); + } + + void showDefinition(BuildContext context) { + if (textSelection == null) return; + OverlayUtil.showPositionedCard( + context: context, + cardToShow: WordDataCard( + word: textSelection!, + wordLang: langCode, + fullText: messageText, + fullTextLang: langCode, + hasInfo: false, + room: room, + ), + cardSize: const Size(300, 300), + transformTargetId: targetId, + backDropToDismiss: false, + ); + } + + // web toolbar + Future showToolbar(BuildContext context) async { + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(targetId); + + final RenderObject? targetRenderBox = + layerLinkAndKey.key.currentContext!.findRenderObject(); + final Offset transformTargetOffset = + (targetRenderBox as RenderBox).localToGlobal(Offset.zero); + + if (dx != null && dx! > MediaQuery.of(context).size.width - 130) { + dx = MediaQuery.of(context).size.width - 130; + } + final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0; + final double yOffset = + dy != null ? dy! - transformTargetOffset.dy + 10 : 10; + + OverlayUtil.showOverlay( + context: context, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.zero, + ), + onPressed: () { + showDefinition(context); + }, + child: Text( + L10n.of(context)!.showDefinition, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + size: const Size(130, 45), + transformTargetId: targetId, + offset: Offset(xOffset, yOffset), + ); + } + + void onMouseRegionUpdate(PointerEvent event) { + dx = event.position.dx; + dy = event.position.dy; + } + + Widget contextMenuOverride({ + required BuildContext context, + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + }) { + if (textSelection == null && contentSelection == null) { + return const SizedBox(); + } + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: textSelection?.contextMenuAnchors ?? + contentSelection!.contextMenuAnchors, + buttonItems: [ + if (textSelection != null) ...textSelection.contextMenuButtonItems, + if (contentSelection != null) + ...contentSelection.contextMenuButtonItems, + ContextMenuButtonItem( + label: L10n.of(context)!.showDefinition, + onPressed: () { + showDefinition(context); + focusNode.unfocus(); + }, + ), + ], + ); + } } diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 782af3d8b..935268d97 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -105,6 +105,7 @@ class PangeaRichTextState extends State { cause: cause, context: context, ), + onTap: () => messageToolbar?.onTextTap(context), focusNode: widget.messageToolbar?.focusNode, contextMenuBuilder: (context, state) => widget.messageToolbar?.contextMenuOverride(