diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ba582158c..4ccb42939 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3874,5 +3874,6 @@ "type": "text", "placeholders": {} }, - "showDefinition": "Show Definition" + "define": "Define", + "listen": "Listen" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4d1eddcbd..c63a6fa83 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:desktop_drop/desktop_drop.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'; @@ -20,14 +20,17 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/report_message.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.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'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; @@ -144,45 +147,45 @@ class ChatController extends State Timer? typingCoolDown; Timer? typingTimeout; bool currentlyTyping = false; - bool dragging = false; - - void onDragEntered(_) => setState(() => dragging = true); - - void onDragExited(_) => setState(() => dragging = false); - - void onDragDone(DropDoneDetails details) async { - setState(() => dragging = false); - final bytesList = await showFutureLoadingDialog( - context: context, - future: () => Future.wait( - details.files.map( - (xfile) => xfile.readAsBytes(), - ), - ), - ); - if (bytesList.error != null) return; - - final matrixFiles = []; - for (var i = 0; i < bytesList.result!.length; i++) { - matrixFiles.add( - MatrixFile( - bytes: bytesList.result![i], - name: details.files[i].name, - ).detectFileType, - ); - } - // #Pangea - if (matrixFiles.isEmpty) return; - // Pangea# - - await showAdaptiveDialog( - context: context, - builder: (c) => SendFileDialog( - files: matrixFiles, - room: room, - ), - ); - } + // #Pangea + // bool dragging = false; + + // void onDragEntered(_) => setState(() => dragging = true); + + // void onDragExited(_) => setState(() => dragging = false); + + // void onDragDone(DropDoneDetails details) async { + // setState(() => dragging = false); + // final bytesList = await showFutureLoadingDialog( + // context: context, + // future: () => Future.wait( + // details.files.map( + // (xfile) => xfile.readAsBytes(), + // ), + // ), + // ); + // if (bytesList.error != null) return; + + // final matrixFiles = []; + // for (var i = 0; i < bytesList.result!.length; i++) { + // matrixFiles.add( + // MatrixFile( + // bytes: bytesList.result![i], + // name: details.files[i].name, + // ).detectFileType, + // ); + // } + // if (matrixFiles.isEmpty) return; + + // await showAdaptiveDialog( + // context: context, + // builder: (c) => SendFileDialog( + // files: matrixFiles, + // room: room, + // ), + // ); + // } + // Pangea# bool get canSaveSelectedEvent => selectedEvents.length == 1 && @@ -1542,7 +1545,51 @@ class ChatController extends State lastState = currentState; return currentState; } - // #Pangea + + List get events => + timeline!.events.where((event) => event.isVisibleInGui).toList(); + + final Map _messageToolbarControllers = {}; + final Map _pangeaMessageEvents = {}; + + PangeaMessageEvent? pangeaMessageEvent(String eventId) { + final Event? event = + events.firstWhereOrNull((event) => event.eventId == eventId); + if (timeline == null || event == null || event.type != EventTypes.Message) { + return null; + } + if (!_pangeaMessageEvents.containsKey(eventId)) { + _pangeaMessageEvents[eventId] = PangeaMessageEvent( + event: event, + timeline: timeline!, + ownMessage: event.senderId == Matrix.of(context).client.userID, + ); + } + return _pangeaMessageEvents[eventId]; + } + + ToolbarDisplayController? messageToolbarController(String eventId) { + final Event? event = + events.firstWhereOrNull((event) => event.eventId == eventId); + if (timeline == null || event == null || event.type != EventTypes.Message) { + return null; + } + + final PangeaMessageEvent? messageEvent = pangeaMessageEvent(eventId); + if (messageEvent == null) return null; + + if (!_messageToolbarControllers.containsKey(event.eventId)) { + _messageToolbarControllers[event.eventId] = ToolbarDisplayController( + targetId: event.eventId, + pangeaMessageEvent: messageEvent, + immersionMode: choreographer.immersionMode, + controller: this, + ); + _messageToolbarControllers[event.eventId]!.setToolbar(); + } + return _messageToolbarControllers[eventId]; + } + // Pangea# @override Widget build(BuildContext context) => ChatView(this); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index e69b7d074..d8fc89ebc 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -24,9 +24,12 @@ class ChatEventList extends StatelessWidget { Widget build(BuildContext context) { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final events = controller.timeline!.events - .where((event) => event.isVisibleInGui) - .toList(); + // #Pangea + // final events = controller.timeline!.events + // .where((event) => event.isVisibleInGui) + // .toList(); + final events = controller.events; + // Pangea# final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of @@ -153,6 +156,7 @@ class ChatEventList extends StatelessWidget { controller.choreographer.messageOptions.selectedDisplayLang, immersionMode: controller.choreographer.immersionMode, definitions: controller.choreographer.definitionsEnabled, + controller: controller, // Pangea# selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index efe45d96f..bd1653fe0 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -58,21 +58,18 @@ class ChatInputRow extends StatelessWidget { ), ) else - // #Pangea - PangeaMessageActions(chatController: controller), - // SizedBox( - // height: 56, - // child: TextButton( - // onPressed: controller.forwardEventsAction, - // child: Row( - // children: [ - // const Icon(Icons.keyboard_arrow_left_outlined), - // Text(L10n.of(context)!.forward), - // ], - // ), - // ), - // ), - // Pangea# + SizedBox( + height: 56, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + const Icon(Icons.keyboard_arrow_left_outlined), + Text(L10n.of(context)!.forward), + ], + ), + ), + ), controller.selectedEvents.length == 1 ? controller.selectedEvents.first .getDisplayEvent(controller.timeline!) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index cb6ce4737..9f20616f3 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,4 @@ import 'package:badges/badges.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -26,10 +25,7 @@ import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; -//#Pangea -// enum _EventContextAction { info, report } -enum _EventContextAction { info, forward, report } -//Pangea# +enum _EventContextAction { info, report } class ChatView extends StatelessWidget { final ChatController controller; @@ -39,9 +35,6 @@ class ChatView extends StatelessWidget { List _appBarActions(BuildContext context) { if (controller.selectMode) { return [ -// #Pangea - LanguageDisplayToggle(controller: controller), - // Pangea# if (controller.canEditSelectedEvents) IconButton( icon: const Icon(Icons.edit_outlined), @@ -85,11 +78,6 @@ class ChatView extends StatelessWidget { case _EventContextAction.report: controller.reportEventAction(); break; - // #Pangea - case _EventContextAction.forward: - controller.forwardEventsAction(); - break; - // Pangea# } }, itemBuilder: (context) => [ @@ -105,17 +93,6 @@ class ChatView extends StatelessWidget { // ], // ), // ), - PopupMenuItem( - value: _EventContextAction.forward, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.forward), - const SizedBox(width: 12), - Text(L10n.of(context)!.forward), - ], - ), - ), // Pangea# if (controller.selectedEvents.single.status.isSent) PopupMenuItem( @@ -248,199 +225,199 @@ class ChatView extends StatelessWidget { : null) // #Pangea : null, - body: DropTarget( - onDragDone: controller.onDragDone, - onDragEntered: controller.onDragEntered, - onDragExited: controller.onDragExited, - child: Stack( - children: [ - SafeArea( - child: Column( - children: [ - TombstoneDisplay(controller), - if (scrollUpBannerEventId != null) - Material( - color: Theme.of(context) - .colorScheme - .surfaceVariant, - shape: Border( - bottom: BorderSide( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), - child: ListTile( - leading: IconButton( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - icon: const Icon(Icons.close), - tooltip: L10n.of(context)!.close, - onPressed: () { - controller - .discardScrollUpBannerEventId(); - controller.setReadMarker(); - }, - ), - title: Text( - L10n.of(context)!.jumpToLastReadMessage, - ), - contentPadding: - const EdgeInsets.only(left: 8), - trailing: TextButton( - onPressed: () { - controller.scrollToEventId( - scrollUpBannerEventId, - ); - controller - .discardScrollUpBannerEventId(); - }, - child: Text(L10n.of(context)!.jump), - ), + body: + // #Pangea + // DropTarget( + // onDragDone: controller.onDragDone, + // onDragEntered: controller.onDragEntered, + // onDragExited: controller.onDragExited, + // child: + // Pangea# + Stack( + children: [ + SafeArea( + child: Column( + children: [ + TombstoneDisplay(controller), + if (scrollUpBannerEventId != null) + Material( + color: Theme.of(context) + .colorScheme + .surfaceVariant, + shape: Border( + bottom: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, ), ), - PinnedEvents(controller), - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator - .adaptive( - strokeWidth: 2, - ), - ); - } - return ChatEventList( - controller: controller, + child: ListTile( + leading: IconButton( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + icon: const Icon(Icons.close), + tooltip: L10n.of(context)!.close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); + }, + ), + title: Text( + L10n.of(context)!.jumpToLastReadMessage, + ), + contentPadding: + const EdgeInsets.only(left: 8), + trailing: TextButton( + onPressed: () { + controller.scrollToEventId( + scrollUpBannerEventId, ); + controller.discardScrollUpBannerEventId(); }, + child: Text(L10n.of(context)!.jump), ), ), ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - // #Pangea - // Container( - ConditionalFlexible( - isScroll: controller.isRowScrollable, - child: ConditionalScroll( - isScroll: controller.isRowScrollable, - child: MeasurableWidget( - onChange: (size, position) { - controller.inputRowSize = size!.height; - }, - child: Container( - // Pangea# - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, + PinnedEvents(controller), + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: (context) { + if (controller.timeline == null) { + return const Center( + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular( - AppConfig.borderRadius, - ), - bottomRight: Radius.circular( - AppConfig.borderRadius, - ), + ); + } + return ChatEventList( + controller: controller, + ); + }, + ), + ), + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + // #Pangea + // Container( + ConditionalFlexible( + isScroll: controller.isRowScrollable, + child: ConditionalScroll( + isScroll: controller.isRowScrollable, + child: MeasurableWidget( + onChange: (size, position) { + controller.inputRowSize = size!.height; + }, + child: Container( + // Pangea# + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular( + AppConfig.borderRadius, ), - elevation: 4, - shadowColor: - Colors.black.withAlpha(64), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.white - : Colors.black, - child: controller - .room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment - .spaceEvenly, - children: [ - TextButton.icon( - style: - TextButton.styleFrom( - padding: - const EdgeInsets - .all(16), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: - controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), + bottomRight: Radius.circular( + AppConfig.borderRadius, + ), + ), + elevation: 4, + shadowColor: Colors.black.withAlpha(64), + clipBehavior: Clip.hardEdge, + color: Theme.of(context).brightness == + Brightness.light + ? Colors.white + : Colors.black, + child: controller + .room.isAbandonedDMRoom == + true + ? Row( + mainAxisAlignment: + MainAxisAlignment + .spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16), + foregroundColor: + Theme.of(context) + .colorScheme + .error, ), - TextButton.icon( - style: - TextButton.styleFrom( - padding: - const EdgeInsets - .all(16), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: controller - .recreateChat, - label: Text( - L10n.of(context)! - .reopenChat, - ), + icon: const Icon( + Icons.archive_outlined, ), - ], - ) - : Column( - mainAxisSize: - MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), - ), + onPressed: + controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: + controller.recreateChat, + label: Text( + L10n.of(context)! + .reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), ), ), ), ), - if (controller.dragging) - Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), - ), - ], - ), + ), + // #Pangea + // if (controller.dragging) + // Container( + // color: Theme.of(context) + // .scaffoldBackgroundColor + // .withOpacity(0.9), + // alignment: Alignment.center, + // child: const Icon( + // Icons.upload_outlined, + // size: 100, + // ), + // ), + // Pangea# + ], ), - ], - ), + ), + ], ), + // ), ); }, ), @@ -481,4 +458,4 @@ class ConditionalScroll extends StatelessWidget { return child; } } -// #Pangea \ No newline at end of file +// #Pangea diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 2ea75ab74..2f43cfdbb 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,10 +1,8 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -21,7 +19,7 @@ class HtmlMessage extends StatelessWidget { final Room room; final Color textColor; // #Pangea - final ShowDefintionUtil? messageToolbar; + // final ShowDefintionUtil? messageToolbar; // Pangea# const HtmlMessage({ @@ -30,7 +28,7 @@ class HtmlMessage extends StatelessWidget { required this.room, this.textColor = Colors.black, // #Pangea - this.messageToolbar, + // this.messageToolbar, // Pangea# }); @@ -101,20 +99,20 @@ class HtmlMessage extends StatelessWidget { // there is no need to pre-validate the html, as we validate it while rendering // #Pangea return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, + // onHover: messageToolbar?.onMouseRegionUpdate, child: SelectionArea( - onSelectionChanged: (SelectedContent? selection) => - messageToolbar?.onTextSelection( - selectedContent: selection, - context: context, - ), - focusNode: messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - messageToolbar?.contextMenuOverride( - context: context, - contentSelection: state, - ) ?? - const SizedBox(), + // onSelectionChanged: (SelectedContent? selection) => + // messageToolbar?.onTextSelection( + // selectedContent: selection, + // context: context, + // ), + // focusNode: messageToolbar?.focusNode, + // contextMenuBuilder: (context, state) => + // messageToolbar?.contextMenuOverride( + // context: context, + // contentSelection: state, + // ) ?? + // const SizedBox(), // Pangea# child: Html.fromElement( documentElement: element as dom.Element, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 36bdb9fee..4f4d06d86 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,7 +1,9 @@ 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/models/language_model.dart'; import 'package:fluffychat/pangea/models/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:fluffychat/utils/string_color.dart'; @@ -9,6 +11,7 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.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'; @@ -40,6 +43,7 @@ class Message extends StatelessWidget { final LanguageModel? selectedDisplayLang; final bool immersionMode; final bool definitions; + final ChatController controller; // Pangea# const Message( @@ -61,6 +65,7 @@ class Message extends StatelessWidget { required this.selectedDisplayLang, required this.immersionMode, required this.definitions, + required this.controller, // Pangea# super.key, }); @@ -138,12 +143,10 @@ class Message extends StatelessWidget { } // #Pangea - final pangeaMessageEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: ownMessage, - selected: selected, - ); + final PangeaMessageEvent? pangeaMessageEvent = + controller.pangeaMessageEvent(event.eventId); + final ToolbarDisplayController? toolbarController = + controller.messageToolbarController(event.eventId); // Pangea# final resetAnimateIn = this.resetAnimateIn; @@ -241,25 +244,13 @@ 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 - ? selected - ? null - : () => print('long press') - : () { - onSelect(event); - // Android usually has a vibration effect on long press: - if (PlatformInfos.isAndroid) { - Vibration.hasVibrator().then((has) { - if (has == true) { - Vibration.vibrate(duration: 50); - } - }); - } - }, + onTap: () => toolbarController?.showToolbar(context), + onDoubleTap: () => + toolbarController?.showToolbar(context), + onLongPress: () { + onSelect(event); + HapticFeedback.selectionClick(); + }, child: AnimatedOpacity( opacity: animateIn ? 0 @@ -356,9 +347,8 @@ class Message extends StatelessWidget { // #Pangea selected: selected, pangeaMessageEvent: pangeaMessageEvent, - selectedDisplayLang: selectedDisplayLang, immersionMode: immersionMode, - definitions: definitions, + toolbarController: toolbarController, // Pangea# ), if (event.hasAggregatedEvents( @@ -366,7 +356,8 @@ class Message extends StatelessWidget { RelationshipTypes.edit, ) // #Pangea || - (pangeaMessageEvent.showUseType) + (pangeaMessageEvent?.showUseType ?? + false) // Pangea# ) Padding( @@ -378,8 +369,9 @@ class Message extends StatelessWidget { children: [ // #Pangea if (pangeaMessageEvent - .showUseType) ...[ - pangeaMessageEvent.useType + ?.showUseType ?? + false) ...[ + pangeaMessageEvent!.useType .iconView( context, textColor.withAlpha(164), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 7a7c0b5bb..fd4791d3c 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -2,7 +2,9 @@ import 'package:fluffychat/pages/chat/events/html_message.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_context_menu.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/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -31,27 +33,24 @@ class MessageContent extends StatelessWidget { final BorderRadius borderRadius; // #Pangea final bool selected; - final PangeaMessageEvent pangeaMessageEvent; + final PangeaMessageEvent? pangeaMessageEvent; //question: are there any performance benefits to using booleans //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 LanguageModel? selectedDisplayLang; final bool immersionMode; - final bool definitions; - ShowDefintionUtil? messageToolbar; + final ToolbarDisplayController? toolbarController; // Pangea# - MessageContent( + const MessageContent( this.event, { this.onInfoTab, super.key, required this.textColor, // #Pangea required this.selected, - required this.pangeaMessageEvent, - required this.selectedDisplayLang, + this.pangeaMessageEvent, required this.immersionMode, - required this.definitions, + required this.toolbarController, // Pangea# required this.borderRadius, }); @@ -124,18 +123,6 @@ class MessageContent extends StatelessWidget { @override Widget build(BuildContext context) { - // #Pangea - messageToolbar = ShowDefintionUtil( - targetId: pangeaMessageEvent.eventId, - room: pangeaMessageEvent.room, - langCode: selectedDisplayLang?.langCode ?? - MatrixState.pangeaController.languageController.activeL2Code( - roomID: pangeaMessageEvent.room.id, - ) ?? - LanguageModel.unknown.langCode, - messageText: "", - ); - // Pangea# final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final buttonTextColor = textColor; switch (event.type) { @@ -186,7 +173,7 @@ class MessageContent extends StatelessWidget { event.isRichMessage // #Pangea && - !pangeaMessageEvent.showRichText + !(pangeaMessageEvent?.showRichText ?? false) // Pangea# ) { var html = event.formattedText; @@ -194,13 +181,13 @@ class MessageContent extends StatelessWidget { html = '* $html'; } // #Pangea - messageToolbar?.messageText = html; + // messageToolbar?.messageText = html; // Pangea# return HtmlMessage( html: html, textColor: textColor, room: event.room, - messageToolbar: messageToolbar, + // messageToolbar: messageToolbar, ); } // else we fall through to the normal message rendering @@ -286,85 +273,86 @@ class MessageContent extends StatelessWidget { decoration: event.redacted ? TextDecoration.lineThrough : null, height: 1.3, ); - if (pangeaMessageEvent.showRichText) { - return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, - child: PangeaRichText( - style: messageTextStyle, - selected: selected, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - definitions: definitions, - selectedDisplayLang: selectedDisplayLang, - messageToolbar: messageToolbar, - ), + if (pangeaMessageEvent?.showRichText ?? false) { + return PangeaRichText( + style: messageTextStyle, + pangeaMessageEvent: pangeaMessageEvent!, + immersionMode: immersionMode, + toolbarController: toolbarController!, + // selectedDisplayLang: selectedDisplayLang, + // highlighted: toolbarController!.highlighted, ); } - return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, - child: FutureBuilder( - // Pangea# - future: event.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ), - builder: (context, snapshot) { - // #Pangea - if (!snapshot.hasData) { - return Text( - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ), - style: messageTextStyle, - ); - } - // return Linkify( - final String messageText = snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ); - messageToolbar?.messageText = messageText; - return SelectableLinkify( - // Pangea# - text: messageText, - focusNode: messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - messageToolbar?.contextMenuOverride( - context: context, - textSelection: state, - ) ?? - const SizedBox(), - // text: snapshot.data ?? - // event.calcLocalizedBodyFallback( - // MatrixLocals(L10n.of(context)!), - // hideReply: true, - // ), - style: TextStyle( - color: textColor, - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: - event.redacted ? TextDecoration.lineThrough : null, + return FutureBuilder( + // Pangea# + future: event.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ), + builder: (context, snapshot) { + // #Pangea + if (!snapshot.hasData) { + return Text( + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, ), - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: textColor.withAlpha(150), - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: TextDecoration.underline, - decorationColor: textColor.withAlpha(150), + style: messageTextStyle, + ); + } + // return Linkify( + final String messageText = snapshot.data ?? + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ); + toolbarController?.toolbar?.textSelection.setMessageText( + messageText, + ); + return SelectableLinkify( + onSelectionChanged: (selection, cause) => toolbarController + ?.toolbar?.textSelection + .onTextSelection(selection), + onTap: () => toolbarController?.showToolbar(context), + // Pangea# + text: toolbarController?.toolbar?.textSelection.messageText ?? + messageText, + focusNode: toolbarController?.focusNode, + contextMenuBuilder: (context, state) => + MessageContextMenu.contextMenuOverride( + context: context, + textSelection: state, + onDefine: () => toolbarController?.showToolbar( + context, + mode: MessageMode.definition, ), - onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - onSelectionChanged: (selection, cause) => - messageToolbar?.onTextSelection( - selectedText: selection, - cause: cause, - context: context, + onListen: () => toolbarController?.showToolbar( + context, + mode: MessageMode.play, ), - onTap: () => messageToolbar?.onTextTap(context), - ); - }, - ), + ), + // text: snapshot.data ?? + // event.calcLocalizedBodyFallback( + // MatrixLocals(L10n.of(context)!), + // hideReply: true, + // ), + style: TextStyle( + color: textColor, + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: textColor.withAlpha(150), + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: TextDecoration.underline, + decorationColor: textColor.withAlpha(150), + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + // onTap: () => messageToolbar?.onTextTap(context), + ); + }, ); } case EventTypes.CallInvite: diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 9775beb3c..b4047e1e4 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -269,6 +269,7 @@ class ITController { completedITSteps.add(itStep); showChoiceFeedback = true; + Future.delayed( const Duration( milliseconds: ChoreoConstants.millisecondsToDisplayFeedback, diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 9fa092215..6f5cca545 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -224,39 +224,40 @@ class ITChoices extends StatelessWidget { int index, [ Color? borderColor, String? choiceFeedback, - ]) => - OverlayUtil.showPositionedCard( - context: context, - cardToShow: choiceFeedback == null - ? WordDataCard( - word: controller.currentITStep!.continuances[index].text, - wordLang: controller.targetLangCode, - fullText: sourceText ?? controller.choreographer.currentText, - fullTextLang: sourceText != null - ? controller.sourceLangCode - : controller.targetLangCode, - hasInfo: controller.currentITStep!.continuances[index].hasInfo, - choiceFeedback: choiceFeedback, - room: controller.choreographer.chatController.room, - ) - : ITFeedbackCard( - req: ITFeedbackRequestModel( - sourceText: sourceText!, - currentText: controller.choreographer.currentText, - chosenContinuance: - controller.currentITStep!.continuances[index].text, - bestContinuance: controller.currentITStep!.best.text, - feedbackLang: controller.targetLangCode, - sourceTextLang: controller.sourceLangCode, - targetLang: controller.targetLangCode, - ), - choiceFeedback: choiceFeedback, + ]) { + OverlayUtil.showPositionedCard( + context: context, + cardToShow: choiceFeedback == null + ? WordDataCard( + word: controller.currentITStep!.continuances[index].text, + wordLang: controller.targetLangCode, + fullText: sourceText ?? controller.choreographer.currentText, + fullTextLang: sourceText != null + ? controller.sourceLangCode + : controller.targetLangCode, + hasInfo: controller.currentITStep!.continuances[index].hasInfo, + choiceFeedback: choiceFeedback, + room: controller.choreographer.chatController.room, + ) + : ITFeedbackCard( + req: ITFeedbackRequestModel( + sourceText: sourceText!, + currentText: controller.choreographer.currentText, + chosenContinuance: + controller.currentITStep!.continuances[index].text, + bestContinuance: controller.currentITStep!.best.text, + feedbackLang: controller.targetLangCode, + sourceTextLang: controller.sourceLangCode, + targetLang: controller.targetLangCode, ), - cardSize: const Size(300, 300), - borderColor: borderColor, - transformTargetId: controller.choreographer.itBarTransformTargetKey, - backDropToDismiss: false, - ); + choiceFeedback: choiceFeedback, + ), + cardSize: const Size(300, 300), + borderColor: borderColor, + transformTargetId: controller.choreographer.itBarTransformTargetKey, + backDropToDismiss: false, + ); + } @override Widget build(BuildContext context) { diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index e60b44b40..558fc55b2 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'dart:math'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/controllers/class_controller.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/language_controller.dart'; @@ -225,19 +224,19 @@ class PangeaController { continue; } final List userIds = participants.map((user) => user.id).toList(); - if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { - try { - await space.invite(BotName.byEnvironment); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${space.id}", - ); - } - } + // if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { + // try { + // await space.invite(BotName.byEnvironment); + // await space.setPower( + // BotName.byEnvironment, + // ClassDefaultValues.powerLevelOfAdmin, + // ); + // } catch (err) { + // ErrorHandler.logError( + // e: "Failed to invite pangea bot to space ${space.id}", + // ); + // } + // } } } } diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index 4db6caa6e..10b204631 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:flutter/material.dart'; import 'package:http/http.dart'; import '../network/requests.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 70d6834ec..f15a9e87d 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -562,7 +562,6 @@ extension PangeaRoom on Room { event: event, timeline: timeline, ownMessage: true, - selected: false, ); msgs.add( RecentMessageRecord( diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 549b8afa5..390912fd5 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -283,10 +283,20 @@ class IGCTextData { nextTokenIndex = matchTokens.length; } - final String matchText = originalInput.substring( - matchTokens[tokenIndex].token.text.offset, - matchTokens[nextTokenIndex - 1].token.end, - ); + String matchText; + try { + matchText = originalInput.substring( + matchTokens[tokenIndex].token.text.offset, + matchTokens[nextTokenIndex - 1].token.end, + ); + } catch (err) { + return [ + TextSpan( + text: originalInput, + style: defaultStyle, + ), + ]; + } items.add( TextSpan( diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index b7caf64d4..d46ad0dc3 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; @@ -25,14 +26,13 @@ class PangeaMessageEvent { late Event _event; final Timeline timeline; final bool ownMessage; - final bool selected; bool _isValidPangeaMessageEvent = true; + RepresentationEvent? _displayRepresentation; PangeaMessageEvent({ required Event event, required this.timeline, required this.ownMessage, - required this.selected, }) { if (event.type != EventTypes.Message) { _isValidPangeaMessageEvent = false; @@ -46,6 +46,8 @@ class PangeaMessageEvent { //the timeline filters the edits and uses the original events //so this event will always be the original and the sdk getter body //handles getting the latest text from the aggregated events + Event get event => _event; + String get body => _event.body; String get senderId => _event.senderId; @@ -79,7 +81,7 @@ class PangeaMessageEvent { if ([EventStatus.error, EventStatus.sending].contains(_event.status)) { return false; } - if (ownMessage && !selected) return false; + // if (ownMessage && !selected) return false; return true; } @@ -87,13 +89,13 @@ class PangeaMessageEvent { //get audio for text and language //if no audio exists, create it //if audio exists, return it - Future getAudioGlobal(String langCode) async { + Future getAudioGlobal(String langCode) async { // try { final String text = representationByLanguage(langCode)?.text ?? body; final local = getAudioLocal(langCode, text); - if (local != null) return Future.value(local.eventId); + if (local != null) return Future.value(local); final TextToSpeechRequest params = TextToSpeechRequest( text: text, @@ -132,69 +134,61 @@ class PangeaMessageEvent { throw Exception("Unexpected mime type: ${file.mimeType}"); } - return room.sendFileEvent( - file, - inReplyTo: _event, - extraContent: { - 'info': { - ...file.info, - 'duration': response.durationMillis, - }, - 'org.matrix.msc3245.voice': {}, - 'org.matrix.msc1767.audio': { - 'duration': response.durationMillis, - 'waveform': response.waveform, + try { + final String? eventId = await room.sendFileEvent( + file, + inReplyTo: _event, + extraContent: { + 'info': { + ...file.info, + 'duration': response.durationMillis, + }, + 'org.matrix.msc3245.voice': {}, + 'org.matrix.msc1767.audio': { + 'duration': response.durationMillis, + 'waveform': response.waveform, + }, + ModelKey.transcription: { + ModelKey.text: text, + ModelKey.langCode: langCode, + }, }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, - }, - ).timeout( - Durations.long4, - onTimeout: () { - debugPrint("timeout in getAudioGlobal"); - return null; - }, - ).then((eventId) { + ); + // .timeout( + // Durations.long4, + // onTimeout: () { + // debugPrint("timeout in getAudioGlobal"); + // return null; + // }, + // ); + debugPrint("eventId in getAudioGlobal $eventId"); - return eventId; - }).catchError((err, s) { + return eventId != null ? room.getEventById(eventId) : null; + } catch (err) { debugPrint("error in getAudioGlobal"); - debugPrint(err); - debugPrint(s); debugger(when: kDebugMode); return null; - }); - - // } catch (err, s) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // e: err, - // s: s, - // ); - // return Future.value(null); - // } + } } Event? getAudioLocal(String langCode, String text) { return allAudio.firstWhereOrNull( (element) { // Safely access the transcription map - final transcription = element.content.tryGet(ModelKey.transcription); + final transcription = element.content.tryGetMap(ModelKey.transcription); - return transcription != null; - // if (transcription == null) { - // // If transcription is null, this element does not match. - // return false; - // } + // return transcription != null; + if (transcription == null) { + // If transcription is null, this element does not match. + return false; + } - // // Safely get language code and text from the transcription - // final elementLangCode = transcription.tryGet(ModelKey.langCode); - // final elementText = transcription.tryGet(ModelKey.text); + // Safely get language code and text from the transcription + final elementLangCode = transcription[ModelKey.langCode]; + final elementText = transcription[ModelKey.text]; - // // Check if both language code and text match - // return elementLangCode == langCode && elementText == text; + // Check if both language code and text match + return elementLangCode == langCode && elementText == text; }, ); } @@ -397,14 +391,53 @@ class PangeaMessageEvent { !room.isUserSpaceAdmin(_event.senderId) && _event.messageType != PangeaMessageTypes.report; + String get messageDisplayLangCode { + final bool immersionMode = MatrixState + .pangeaController.permissionsController + .isToolEnabled(ToolSetting.immersionMode, room); + final String? l2Code = MatrixState.pangeaController.languageController + .activeL2Code(roomID: room.id); + + final String? langCode = immersionMode ? l2Code : originalWritten?.langCode; + return langCode ?? LanguageKeys.unknownLanguage; + } + + Future getDisplayRepresentation( + BuildContext context, + ) async { + if (messageDisplayLangCode == LanguageKeys.unknownLanguage) return null; + if (_displayRepresentation != null) return _displayRepresentation; + _displayRepresentation = representationByLanguage(messageDisplayLangCode); + if (_displayRepresentation != null) { + return _displayRepresentation; + } + + try { + _displayRepresentation = await representationByLanguageGlobal( + context: context, + langCode: messageDisplayLangCode, + ); + return _displayRepresentation; + } catch (err, s) { + ErrorHandler.logError( + m: "error in getDisplayRepresentation", + e: err, + s: s, + ); + return null; + } + } + + String get displayMessageText => _displayRepresentation?.text ?? body; + // List get activities => //each match is turned into an activity that other students can access //they're not told the answer but have to find it themselves //the message has a blank piece which they fill in themselves // replication of logic from message_content.dart - bool get isHtml => - AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + // bool get isHtml => + // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; } class URLFinder { diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 912376e45..d559065ee 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/config/environment.dart'; /// https://api.staging.pangea.chat/api/v1/ class PApiUrls { static String baseAPI = Environment.baseAPI; - static String choreoBaseApi = Environment.choreoApi; /// ---------------------- Languages -------------------------------------- static String getLanguages = "/languages"; @@ -51,7 +50,7 @@ class PApiUrls { static String firstStep = "/it_initialstep"; static String subseqStep = "/it_step"; - static String textToSpeech = "$choreoBaseApi/text_to_speech"; + static String textToSpeech = "${Environment.choreoApi}/text_to_speech"; ///-------------------------------- revenue cat -------------------------- static String rcApiV1 = "https://api.revenuecat.com/v1"; diff --git a/lib/pangea/repo/interactive_translation_repo.dart b/lib/pangea/repo/interactive_translation_repo.dart index b46db4b75..ab5d3b1d6 100644 --- a/lib/pangea/repo/interactive_translation_repo.dart +++ b/lib/pangea/repo/interactive_translation_repo.dart @@ -14,7 +14,7 @@ class ITRepo { CustomInputRequestModel initalText, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); final Response res = @@ -29,7 +29,7 @@ class ITRepo { SystemChoiceRequestModel subseqText, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); diff --git a/lib/pangea/repo/message_service.repo.dart b/lib/pangea/repo/message_service.repo.dart index 612ee0c61..ce51a3802 100644 --- a/lib/pangea/repo/message_service.repo.dart +++ b/lib/pangea/repo/message_service.repo.dart @@ -8,7 +8,7 @@ class MessageServiceRepo { String messageId, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index fd6124420..d951ab449 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/pangea/utils/download_chat.dart b/lib/pangea/utils/download_chat.dart index c1c28935d..9e3d4d423 100644 --- a/lib/pangea/utils/download_chat.dart +++ b/lib/pangea/utils/download_chat.dart @@ -136,7 +136,6 @@ List getPangeaMessageEvents( event: message, timeline: timeline, ownMessage: false, - selected: false, ), ) .cast() diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index a1ce04349..82b3461c4 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -17,9 +17,11 @@ class GetChatListItemSubtitle { ) async { if (event == null) return L10n.of(context)!.emptyChat; // try { - if (event.type != EventTypes.Message || - !pangeaController.permissionsController - .isToolEnabled(ToolSetting.immersionMode, event.room)) { + if (event.type != EventTypes.Message) + // || + // !pangeaController.permissionsController + // .isToolEnabled(ToolSetting.immersionMode, event.room)) + { return event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), hideReply: true, @@ -31,13 +33,17 @@ class GetChatListItemSubtitle { ); } + String? eventContextId = event.eventId; + if (!event.eventId.isValidMatrixId || event.eventId.sigil != '\$') { + eventContextId = null; + } final Timeline timeline = - await event.room.getTimeline(eventContextId: event.eventId); + await event.room.getTimeline(eventContextId: eventContextId); + final PangeaMessageEvent pangeaMessageEvent = PangeaMessageEvent( event: event, timeline: timeline, ownMessage: false, - selected: false, ); final l2Code = pangeaController.languageController.activeL2Code(roomID: event.roomId); diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 2d8c465eb..26399c8aa 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -15,11 +15,16 @@ class OverlayUtil { static showOverlay({ required BuildContext context, required Widget child, - required Size size, required String transformTargetId, + // Size? size, + double? width, + double? height, Offset? offset, backDropToDismiss = true, Color? borderColor, + Color? backgroundColor, + Alignment? targetAnchor, + Alignment? followerAnchor, }) { try { MatrixState.pAnyState.closeOverlay(); @@ -27,35 +32,37 @@ class OverlayUtil { MatrixState.pAnyState.layerLinkAndKey(transformTargetId); final OverlayEntry entry = OverlayEntry( - builder: (context) => Stack( - children: [ - // GestureDetector to detect when dismissed by clicking outside - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - MatrixState.pAnyState.closeOverlay(); - }, + builder: (context) => AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: Stack( + children: [ + if (backDropToDismiss) + TransparentBackdrop( + backgroundColor: backgroundColor, + ), + Positioned( + width: width, + height: height, + child: CompositedTransformFollower( + targetAnchor: targetAnchor ?? Alignment.topLeft, + followerAnchor: followerAnchor ?? Alignment.topLeft, + link: layerLinkAndKey.link, + showWhenUnlinked: false, + offset: offset ?? Offset.zero, + child: child, + ), ), - ), - if (backDropToDismiss) const TransparentBackdrop(), - Positioned( - width: size.width, - height: size.height, - child: CompositedTransformFollower( - link: layerLinkAndKey.link, - showWhenUnlinked: false, - offset: offset ?? Offset.zero, - child: child, - ), - ), - ], + ], + ), ), ); MatrixState.pAnyState.openOverlay(entry, context); } catch (err, stack) { - debugger(when: kDebugMode); + debugPrint("ERROR: $err"); + debugPrint("STACK: $stack"); + // debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); } } @@ -90,7 +97,8 @@ class OverlayUtil { showOverlay( context: context, child: child, - size: cardSize, + width: cardSize.width, + height: cardSize.height, transformTargetId: transformTargetId, offset: cardOffset, backDropToDismiss: backDropToDismiss, @@ -174,15 +182,17 @@ class OverlayUtil { } class TransparentBackdrop extends StatelessWidget { + final Color? backgroundColor; const TransparentBackdrop({ super.key, + this.backgroundColor, }); @override Widget build(BuildContext context) { return Material( borderOnForeground: false, - color: Colors.transparent, + color: backgroundColor ?? Colors.transparent, clipBehavior: Clip.antiAlias, child: InkWell( hoverColor: Colors.transparent, diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart deleted file mode 100644 index 6aa3ba29f..000000000 --- a/lib/pangea/utils/show_defintion_util.dart +++ /dev/null @@ -1,159 +0,0 @@ -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'; - -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/utils/toolbar_util.dart b/lib/pangea/utils/toolbar_util.dart deleted file mode 100644 index 4848269fb..000000000 --- a/lib/pangea/utils/toolbar_util.dart +++ /dev/null @@ -1,303 +0,0 @@ -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 MessageOverlayController { - OverlayEntry? _overlayEntry; - final BuildContext _context; - final GlobalKey _targetKey; - MessageMode? _currentMode; - AnimationController? _animationController; - - MessageOverlayController(this._context, this._targetKey) { - _animationController = AnimationController( - vsync: Navigator.of(_context), // Using the Navigator's TickerProvider - duration: const Duration(milliseconds: 300), - ); - } - - 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; - case MessageMode.play: - return Icons.play_arrow; - case MessageMode.definition: - return Icons.book; - case MessageMode.image: - return Icons.image; - case MessageMode.spellCheck: - return Icons.spellcheck; - default: - 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/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart new file mode 100644 index 000000000..dd306be54 --- /dev/null +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -0,0 +1,149 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.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 MessageAudioCard extends StatefulWidget { + final PangeaMessageEvent messageEvent; + + const MessageAudioCard({ + super.key, + required this.messageEvent, + }); + + @override + MessageAudioCardState createState() => MessageAudioCardState(); +} + +class MessageAudioCardState extends State { + // RepresentationEvent? repEvent; + bool _isLoading = false; + Event? localAudioEvent; + // String langCode = "en"; + + // void setLangCode() { + // final String? l2Code = + // MatrixState.pangeaController.languageController.activeL2Code( + // roomID: widget.messageEvent.room.id, + // ); + // setState(() => langCode = l2Code ?? "en"); + // } + + // void fetchRepresentation(BuildContext context) { + // repEvent = widget.messageEvent.representationByLanguage( + // langCode, + // ); + + // if (repEvent == null) { + // setState(() => _isLoading = true); + // widget.messageEvent + // .representationByLanguageGlobal( + // context: context, + // langCode: langCode, + // ) + // .onError((error, stackTrace) => ErrorHandler.logError()) + // .then(((RepresentationEvent? event) => repEvent = event)) + // .whenComplete( + // () => setState(() => _isLoading = false), + // ); + // } + // } + + void fetchAudio() { + if (!mounted) return; + // final String? text = widget.messageEvent.displayMessageText; + // if (text == null || text.isEmpty) return; + setState(() => _isLoading = true); + + widget.messageEvent + .getAudioGlobal(widget.messageEvent.messageDisplayLangCode) + .then((Event? event) { + localAudioEvent = event; + }).catchError((e) { + if (!mounted) return null; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.errorGettingAudio), + ), + ); + return null; + }).whenComplete(() { + if (mounted) setState(() => _isLoading = false); + }); + } + + @override + void initState() { + super.initState(); + widget.messageEvent + .getDisplayRepresentation(context) + .then((_) => fetchAudio()); + } + + @override + Widget build(BuildContext context) { + final playButton = InkWell( + borderRadius: BorderRadius.circular(64), + onTap: () => widget.messageEvent + .getDisplayRepresentation(context) + .then((event) => event == null ? null : fetchAudio), + child: Material( + color: AppConfig.primaryColor.withAlpha(64), + borderRadius: BorderRadius.circular(64), + child: const Icon( + // Change the icon based on some condition. If you have an audio player state, use it here. + Icons.play_arrow_outlined, + color: AppConfig.primaryColor, + ), + ), + ); + + return Padding( + padding: const EdgeInsets.all(8), + child: _isLoading + ? SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ) + : localAudioEvent != null + ? Container( + constraints: const BoxConstraints( + maxWidth: 250, + ), + child: Column( + children: [ + AudioPlayerWidget( + localAudioEvent!, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ], + ), + ) + : + // Opacity( + // opacity: widget.messageEvent.getDisplayRepresentation().then((event) => event == null ? ) == null + // ? 0.5 + // : 1, + // // child: SizedBox( + // // width: 44, + // // height: 36, + // child: + Padding( + padding: const EdgeInsets.only(left: 8), + child: playButton, + ), + // ), + // ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_context_menu.dart b/lib/pangea/widgets/chat/message_context_menu.dart new file mode 100644 index 000000000..5bb28dbe7 --- /dev/null +++ b/lib/pangea/widgets/chat/message_context_menu.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MessageContextMenu { + static List customToolbarOptions( + BuildContext context, + void Function()? onDefine, + void Function()? onListen, + ) { + return [ + ContextMenuButtonItem( + label: L10n.of(context)!.define, + onPressed: onDefine, + ), + ContextMenuButtonItem( + label: L10n.of(context)!.listen, + onPressed: onListen, + ), + ]; + } + + static List toolbarOptions( + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + BuildContext context, + void Function()? onDefine, + void Function()? onListen, + ) { + final List menuItems = + textSelection?.contextMenuButtonItems ?? + contentSelection?.contextMenuButtonItems ?? + []; + menuItems.sort((a, b) { + if (a.type == ContextMenuButtonType.copy) return -1; + if (b.type == ContextMenuButtonType.copy) return 1; + return 0; + }); + return MessageContextMenu.customToolbarOptions( + context, onDefine, onListen) + + menuItems; + } + + static Widget contextMenuOverride({ + required BuildContext context, + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + void Function()? onDefine, + void Function()? onListen, + }) { + if (textSelection == null && contentSelection == null) { + return const SizedBox(); + } + + final List menuItems = + MessageContextMenu.toolbarOptions( + textSelection, + contentSelection, + context, + onDefine, + onListen, + ); + + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: textSelection?.contextMenuAnchors ?? + contentSelection!.contextMenuAnchors, + buttonItems: menuItems, + ); + } +} diff --git a/lib/pangea/widgets/chat/message_text_selection.dart b/lib/pangea/widgets/chat/message_text_selection.dart new file mode 100644 index 000000000..16982d979 --- /dev/null +++ b/lib/pangea/widgets/chat/message_text_selection.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MessageTextSelection { + String? selectedText; + String messageText = ""; + + void setMessageText(String text) { + messageText = text; + } + + void onTextSelection(TextSelection selection) => selection.isCollapsed == true + ? clearTextSelection() + : setTextSelection(selection); + + void setTextSelection(TextSelection selection) { + selectedText = selection.textInside(messageText); + if (BrowserContextMenu.enabled && kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + // selectionStream.add(selectedText); + } + + void clearTextSelection() { + selectedText = null; + if (kIsWeb && !BrowserContextMenu.enabled) { + BrowserContextMenu.enableContextMenu(); + } + // selectionStream.add(selectedText); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart new file mode 100644 index 000000000..4dc2ac4b6 --- /dev/null +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -0,0 +1,303 @@ +import 'dart:async'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/models/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/utils/overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_audio_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/overlay_message.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +enum MessageMode { translation, play, definition } + +class ToolbarDisplayController { + final FocusNode focusNode = FocusNode(); + final PangeaMessageEvent pangeaMessageEvent; + final String targetId; + final bool immersionMode; + final ChatController controller; + + MessageToolbar? toolbar; + String? overlayId; + double? messageWidth; + + final toolbarModeStream = StreamController.broadcast(); + + ToolbarDisplayController({ + required this.pangeaMessageEvent, + required this.targetId, + required this.immersionMode, + required this.controller, + }); + + void setToolbar() { + toolbar ??= MessageToolbar( + textSelection: MessageTextSelection(), + room: pangeaMessageEvent.room, + toolbarModeStream: toolbarModeStream, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: immersionMode, + controller: controller, + ); + + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(targetId); + final targetRenderBox = + layerLinkAndKey.key.currentContext?.findRenderObject(); + if (targetRenderBox == null) return; + final Size transformTargetSize = (targetRenderBox as RenderBox).size; + messageWidth = transformTargetSize.width; + } + + void showToolbar(BuildContext context, {MessageMode? mode}) { + if (highlighted) return; + if (controller.selectMode) { + controller.clearSelectedEvents(); + } + focusNode.unfocus(); + Widget overlayEntry; + try { + overlayEntry = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: pangeaMessageEvent.ownMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + toolbar!, + OverlayMessage( + pangeaMessageEvent.event, + timeline: pangeaMessageEvent.timeline, + immersionMode: immersionMode, + ownMessage: pangeaMessageEvent.ownMessage, + toolbarController: this, + width: messageWidth, + ), + ], + ); + } catch (err) { + ErrorHandler.logError(e: err, s: StackTrace.current); + return; + } + OverlayUtil.showOverlay( + context: context, + child: overlayEntry, + transformTargetId: targetId, + targetAnchor: pangeaMessageEvent.ownMessage + ? Alignment.bottomRight + : Alignment.bottomLeft, + followerAnchor: pangeaMessageEvent.ownMessage + ? Alignment.bottomRight + : Alignment.bottomLeft, + backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(164), + ); + + if (MatrixState.pAnyState.overlay != null) { + overlayId = MatrixState.pAnyState.overlay.hashCode.toString(); + } + + if (mode != null) { + Future.delayed( + const Duration(milliseconds: 100), + () => toolbarModeStream.add(mode), + ); + } + } + + bool get highlighted => + MatrixState.pAnyState.overlay.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; + + const MessageToolbar({ + super.key, + required this.textSelection, + required this.room, + required this.pangeaMessageEvent, + required this.toolbarModeStream, + required this.immersionMode, + required this.controller, + }); + + @override + MessageToolbarState createState() => MessageToolbarState(); +} + +class MessageToolbarState extends State { + Widget? child; + MessageMode? _currentMode; + late StreamSubscription _selectionStream; + late StreamSubscription _toolbarModeStream; + + IconData _getIconData(MessageMode mode) { + switch (mode) { + case MessageMode.translation: + return Icons.g_translate; + case MessageMode.play: + return Icons.play_arrow; + case MessageMode.definition: + return Icons.book; + default: + return Icons.error; // Icon to indicate an error or unsupported mode + } + } + + bool _enabledButton(MessageMode mode) { + switch (mode) { + case MessageMode.translation: + return true; + case MessageMode.play: + return true; + case MessageMode.definition: + debugPrint("checking"); + return widget.textSelection.selectedText != null; + default: + return false; + } + } + + void updateMode(MessageMode newMode) { + debugPrint("updating toolbar mode"); + setState(() => _currentMode = newMode); + switch (_currentMode) { + case MessageMode.translation: + showTranslation(); + break; + case MessageMode.play: + playAudio(); + break; + case MessageMode.definition: + showDefinition(); + break; + default: + break; + } + setState(() {}); + } + + void showTranslation() { + debugPrint("show translation"); + child = MessageTranslationCard( + messageEvent: widget.pangeaMessageEvent, + immersionMode: widget.immersionMode, + ); + } + + void playAudio() { + debugPrint("play audio"); + child = MessageAudioCard( + messageEvent: widget.pangeaMessageEvent, + ); + } + + void showDefinition() { + if (widget.textSelection.selectedText == null || + widget.textSelection.selectedText!.isEmpty) { + return; + } + + child = WordDataCard( + word: widget.textSelection.selectedText!, + wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, + fullText: widget.textSelection.messageText, + fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, + hasInfo: false, + room: widget.room, + ); + } + + void showImage() {} + + void spellCheck() {} + + void showMore() { + MatrixState.pAnyState.closeOverlay(); + widget.controller.onSelectMessage(widget.pangeaMessageEvent.event); + } + + @override + void initState() { + super.initState(); + _toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) { + updateMode(mode); + }); + } + + @override + void dispose() { + _selectionStream.cancel(); + _toolbarModeStream.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), + ), + constraints: const BoxConstraints( + maxWidth: 300, + minWidth: 300, + maxHeight: 300, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Column( + children: [ + child ?? const SizedBox(), + SizedBox(height: child == null ? 0 : 20), + ], + ), + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + return IconButton( + icon: Icon(_getIconData(mode)), + onPressed: + _enabledButton(mode) ? () => updateMode(mode) : null, + ); + }).toList() + + [ + IconButton( + icon: Icon(Icons.adaptive.more_outlined), + onPressed: showMore, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart new file mode 100644 index 000000000..5cb22e919 --- /dev/null +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -0,0 +1,103 @@ +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MessageTranslationCard extends StatefulWidget { + final PangeaMessageEvent messageEvent; + final bool immersionMode; + + const MessageTranslationCard({ + super.key, + required this.messageEvent, + required this.immersionMode, + }); + + @override + MessageTranslationCardState createState() => MessageTranslationCardState(); +} + +class MessageTranslationCardState extends State { + RepresentationEvent? repEvent; + bool _fetchingRepresentation = false; + + String? translationLangCode() { + final String? l1Code = + MatrixState.pangeaController.languageController.activeL1Code( + roomID: widget.messageEvent.room.id, + ); + if (widget.immersionMode) return l1Code; + + final String? l2Code = + MatrixState.pangeaController.languageController.activeL2Code( + roomID: widget.messageEvent.room.id, + ); + final String? originalWrittenCode = + widget.messageEvent.originalWritten?.content.langCode; + return l1Code == originalWrittenCode ? l2Code : l1Code; + } + + void fetchRepresentation(BuildContext context) { + final String? langCode = translationLangCode(); + if (langCode == null) return; + + repEvent = widget.messageEvent.representationByLanguage( + langCode, + ); + + if (repEvent == null && mounted) { + setState(() => _fetchingRepresentation = true); + widget.messageEvent + .representationByLanguageGlobal( + context: context, + langCode: langCode, + ) + .onError( + (error, stackTrace) => ErrorHandler.logError( + e: error, + s: stackTrace, + ), + ) + .then((RepresentationEvent? event) => repEvent = event) + .whenComplete( + () => setState(() => _fetchingRepresentation = false), + ); + } else { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + fetchRepresentation(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: _fetchingRepresentation + ? SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ) + : repEvent != null + ? Text( + repEvent!.text, + style: BotStyle.text(context), + ) + : Text( + L10n.of(context)!.oopsSomethingWentWrong, + style: BotStyle.text(context), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart new file mode 100644 index 000000000..068908ba6 --- /dev/null +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -0,0 +1,119 @@ +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/message_content.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.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; + // #Pangea + // final LanguageModel? selectedDisplayLang; + final bool immersionMode; + // final bool definitions; + final bool ownMessage; + final ToolbarDisplayController toolbarController; + final double? width; + // Pangea# + + const OverlayMessage( + this.event, { + this.selected = false, + required this.timeline, + // #Pangea + // required this.selectedDisplayLang, + required this.immersionMode, + // required this.definitions, + required this.ownMessage, + required this.toolbarController, + this.width, + // Pangea# + 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.surfaceVariant; + final textColor = ownMessage + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground; + + final borderRadius = BorderRadius.only( + topLeft: !ownMessage + ? const Radius.circular(4) + : const Radius.circular(AppConfig.borderRadius), + topRight: const Radius.circular(AppConfig.borderRadius), + bottomLeft: const Radius.circular(AppConfig.borderRadius), + bottomRight: ownMessage + ? const Radius.circular(4) + : const Radius.circular(AppConfig.borderRadius), + ); + 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.primaryContainer; + } + + // #Pangea + final pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: ownMessage, + ); + // Pangea# + + 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, + ), + child: MessageContent( + event, + textColor: textColor, + borderRadius: borderRadius, + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + // selectedDisplayLang: selectedDisplayLang, + immersionMode: immersionMode, + // definitions: definitions, + toolbarController: toolbarController, + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart index e591a448d..ec3dbd0db 100644 --- a/lib/pangea/widgets/chat/text_to_speech_button.dart +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -45,7 +45,6 @@ class _TextToSpeechButtonState extends State { timeline: widget.controller.timeline!, ownMessage: widget.selectedEvent.senderId == Matrix.of(context).client.userID, - selected: true, ); } diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 935268d97..a2d32adb2 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,44 +1,29 @@ -import 'dart:developer'; import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat/events/html_message.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.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 'package:sentry_flutter/sentry_flutter.dart'; - import '../../models/pangea_match_model.dart'; -import '../../models/pangea_representation_event.dart'; -import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; - final TextStyle? style; - final bool selected; - final LanguageModel? selectedDisplayLang; final bool immersionMode; - final bool definitions; - final Choreographer? choreographer; - final ShowDefintionUtil? messageToolbar; + final ToolbarDisplayController toolbarController; + final TextStyle? style; const PangeaRichText({ super.key, required this.pangeaMessageEvent, - required this.selected, - required this.selectedDisplayLang, required this.immersionMode, - required this.definitions, - this.choreographer, + required this.toolbarController, this.style, - this.messageToolbar, }); @override @@ -47,93 +32,73 @@ class PangeaRichText extends StatefulWidget { class PangeaRichTextState extends State { final PangeaController pangeaController = MatrixState.pangeaController; + RepresentationEvent? repEvent; bool _fetchingRepresentation = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; - String textSpan = ""; @override void initState() { super.initState(); - updateTextSpan(); + setTextSpan(); } - @override - void didUpdateWidget(PangeaRichText oldWidget) { - super.didUpdateWidget(oldWidget); - updateTextSpan(); - } + Future setTextSpan() async { + setState(() => _fetchingRepresentation = true); + try { + await widget.pangeaMessageEvent.getDisplayRepresentation(context); + } catch (err) { + ErrorHandler.logError(e: err); + } + setState(() => _fetchingRepresentation = false); - void updateTextSpan() { - setState(() { - textSpan = getTextSpan(context); - widget.messageToolbar?.messageText = textSpan; - }); + widget.toolbarController.toolbar?.textSelection.setMessageText( + widget.pangeaMessageEvent.displayMessageText, + ); } @override Widget build(BuildContext context) { //TODO - take out of build function of every message - // if (areLanguagesSet) { - - if (!widget.selected && - widget.selectedDisplayLang != null && - widget.selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) { - pangeaController.instructions.show( - context, - InstructionsEnum.clickMessage, - widget.pangeaMessageEvent.eventId, - ); - } else if (blur > 0) { - pangeaController.instructions.show( - context, - InstructionsEnum.blurMeansTranslate, - widget.pangeaMessageEvent.eventId, - ); - } - - final Widget richText = widget.pangeaMessageEvent.isHtml - ? HtmlMessage( - html: textSpan, - room: widget.pangeaMessageEvent.room, - textColor: widget.style?.color ?? Colors.black, - messageToolbar: widget.messageToolbar, - ) - : SelectableText.rich( - onSelectionChanged: (selection, cause) => - widget.messageToolbar?.onTextSelection( - selectedText: selection, - cause: cause, - context: context, - ), - onTap: () => messageToolbar?.onTextTap(context), - focusNode: widget.messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - widget.messageToolbar?.contextMenuOverride( - context: context, - textSelection: state, - ) ?? - const SizedBox(), - TextSpan( - text: textSpan, - style: widget.style, - children: [ - if (widget.selected && (_fetchingRepresentation)) - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(left: 5.0), - child: SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.secondaryColor, - ), - ), - ), + final Widget richText = SelectableText.rich( + onSelectionChanged: (selection, cause) => widget + .toolbarController.toolbar?.textSelection + .onTextSelection(selection), + onTap: () => widget.toolbarController.showToolbar(context), + focusNode: widget.toolbarController.focusNode, + contextMenuBuilder: (context, state) => + MessageContextMenu.contextMenuOverride( + context: context, + textSelection: state, + onDefine: () => widget.toolbarController.showToolbar( + context, + mode: MessageMode.definition, + ), + onListen: () => widget.toolbarController.showToolbar( + context, + mode: MessageMode.play, + ), + ), + TextSpan( + text: widget.pangeaMessageEvent.displayMessageText, + style: widget.style, + children: [ + if (_fetchingRepresentation) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 5.0), + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.secondaryColor, ), - ], + ), + ), ), - ); + ], + ), + ); return blur > 0 ? ImageFiltered( @@ -143,55 +108,6 @@ class PangeaRichTextState extends State { : richText; } - String getTextSpan(BuildContext context) { - final String? displayLangCode = - widget.selected ? widget.selectedDisplayLang?.langCode : userL2LangCode; - - if (displayLangCode == null || !widget.immersionMode) { - return widget.pangeaMessageEvent.body; - } - - if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { - debugger(when: kDebugMode); - return widget.pangeaMessageEvent.body; - } - - final RepresentationEvent? repEvent = - widget.pangeaMessageEvent.representationByLanguage( - displayLangCode, - ); - - if (repEvent == null) { - _fetchingRepresentation = true; - - setState(() => {}); - widget.pangeaMessageEvent - .representationByLanguageGlobal( - context: context, - langCode: displayLangCode, - ) - .onError((error, stackTrace) => ErrorHandler.logError()) - .whenComplete(() => setState(() => _fetchingRepresentation = false)); - return widget.pangeaMessageEvent.body; - } - - if (repEvent.event?.eventId.contains("web") ?? false) { - Sentry.addBreadcrumb( - Breadcrumb.fromJson({"repEvent.event": repEvent.event?.toJson()}), - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "representationByLanguageGlobal returned RepEvent with event ID containing 'web' - ${repEvent.event?.eventId}", - ), - ); - } - - return widget.pangeaMessageEvent.isHtml - ? repEvent.formatBody() ?? repEvent.text - : repEvent.text; - } - bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 8c1940aae..2ef498887 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -79,6 +79,18 @@ class WordDataCardController extends State { super.initState(); } + @override + void didUpdateWidget(covariant WordDataCard oldWidget) { + if (oldWidget.word != widget.word) { + if (!widget.hasInfo) { + getContextualDefinition(); + } else { + getWordNet(); + } + } + super.didUpdateWidget(oldWidget); + } + Future getContextualDefinition() async { ContextualDefinitionRequestModel? req; try { @@ -89,7 +101,14 @@ class WordDataCardController extends State { fullTextLang: widget.fullTextLang, wordLang: widget.wordLang, ); - if (mounted) setState(() => isLoadingContextualDefinition = true); + if (!mounted) return; + + setState(() { + contextualDefinitionRes = null; + definitionError = null; + isLoadingContextualDefinition = true; + }); + contextualDefinitionRes = await controller.definitions.get(req); if (contextualDefinitionRes == null) { definitionError = Exception("Error getting definition"); @@ -159,54 +178,57 @@ class WordDataCardView extends StatelessWidget { return Scrollbar( thumbVisibility: true, controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CardHeader( - text: controller.widget.word, - botExpression: BotExpression.down, - ), - if (controller.widget.choiceFeedback != null) - Text( - controller.widget.choiceFeedback!, - style: BotStyle.text(context), - ), - const SizedBox(height: 5.0), - if (controller.wordData != null && controller.wordNetError == null) - WordNetInfo( - wordData: controller.wordData!, - activeL1: controller.activeL1!, - activeL2: controller.activeL2!, + child: Expanded( + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: controller.widget.word, + botExpression: BotExpression.down, ), - if (controller.isLoadingWordNet) const PCircular(), - const SizedBox(height: 5.0), - // if (controller.widget.hasInfo && - // !controller.isLoadingContextualDefinition && - // controller.contextualDefinitionRes == null) - // Material( - // type: MaterialType.transparency, - // child: ListTile( - // leading: const BotFace( - // width: 40, expression: BotExpression.surprised), - // title: Text(L10n.of(context)!.askPangeaBot), - // onTap: controller.handleGetDefinitionButtonPress, - // ), - // ), - if (controller.isLoadingContextualDefinition) const PCircular(), - if (controller.contextualDefinitionRes != null) - Text( - controller.contextualDefinitionRes!.text, - style: BotStyle.text(context), - ), - if (controller.definitionError != null) - Text( - L10n.of(context)!.sorryNoResults, - style: BotStyle.text(context), - ), - ], + if (controller.widget.choiceFeedback != null) + Text( + controller.widget.choiceFeedback!, + style: BotStyle.text(context), + ), + const SizedBox(height: 5.0), + if (controller.wordData != null && + controller.wordNetError == null) + WordNetInfo( + wordData: controller.wordData!, + activeL1: controller.activeL1!, + activeL2: controller.activeL2!, + ), + if (controller.isLoadingWordNet) const PCircular(), + const SizedBox(height: 5.0), + // if (controller.widget.hasInfo && + // !controller.isLoadingContextualDefinition && + // controller.contextualDefinitionRes == null) + // Material( + // type: MaterialType.transparency, + // child: ListTile( + // leading: const BotFace( + // width: 40, expression: BotExpression.surprised), + // title: Text(L10n.of(context)!.askPangeaBot), + // onTap: controller.handleGetDefinitionButtonPress, + // ), + // ), + if (controller.isLoadingContextualDefinition) const PCircular(), + if (controller.contextualDefinitionRes != null) + Text( + controller.contextualDefinitionRes!.text, + style: BotStyle.text(context), + ), + if (controller.definitionError != null) + Text( + L10n.of(context)!.sorryNoResults, + style: BotStyle.text(context), + ), + ], + ), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index f39c96502..25e4ca8c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,14 +337,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.3" - desktop_drop: - dependency: "direct main" - description: - name: desktop_drop - sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d - url: "https://pub.dev" - source: hosted - version: "0.4.4" desktop_lifecycle: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d37aa3845..24f5e0ac8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: chewie: ^1.7.1 collection: ^1.17.2 cupertino_icons: any - desktop_drop: ^0.4.4 + # desktop_drop: ^0.4.4 desktop_notifications: ^0.6.3 device_info_plus: ^9.1.0 dynamic_color: ^1.6.8