From 0f3e1c7e16ed71555e74dace37a70a2c2ecbf293 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 16 Jan 2024 15:51:51 -0500 Subject: [PATCH 1/4] inital work for selecting definitions --- .vscode/settings.json | 6 +- assets/l10n/intl_en.arb | 5 +- lib/pages/chat/events/message.dart | 11 +- lib/pages/chat/events/message_content.dart | 11 +- lib/pangea/models/igc_text_data_model.dart | 234 +++++++++--------- lib/pangea/models/pangea_match_model.dart | 9 +- lib/pangea/models/pangea_token_model.dart | 3 +- lib/pangea/utils/any_state_holder.dart | 7 +- lib/pangea/utils/overlay.dart | 93 +++++-- lib/pangea/utils/show_defintion_util.dart | 130 ++++++++++ lib/pangea/widgets/igc/pangea_rich_text.dart | 166 ++++++++++--- .../widgets/igc/pangea_text_controller.dart | 42 ++-- needed-translations.txt | 139 +++++++---- pubspec.yaml | 1 + 14 files changed, 585 insertions(+), 272 deletions(-) create mode 100644 lib/pangea/utils/show_defintion_util.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index dd15e28ee..59cdec5c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { "dart.previewLsp": true, "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true, - "source.sortMembers": false + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "never" }, "editor.formatOnSave": true } \ No newline at end of file diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a859c6229..1330fdb16 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3956,5 +3956,6 @@ "inNoSpaces": "You are not a member of any classes or exchanges", "successfullySubscribed": "You have successfully subscribed!", "clickToManageSubscription": "Click here to manage your subscription.", - "emptyInviteWarning": "Add this chat to a class or exchange to invite other users." -} \ No newline at end of file + "emptyInviteWarning": "Add this chat to a class or exchange to invite other users.", + "showDefinition": "Show Definition" +} diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 8fc28bd42..2ff8a6605 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,9 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; -import 'package:swipe_to_action/swipe_to_action.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; @@ -12,6 +6,11 @@ import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:swipe_to_action/swipe_to_action.dart'; + import '../../../config/app_config.dart'; import 'message_content.dart'; import 'message_reactions.dart'; diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index a498d22e2..69cc94860 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,9 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:matrix/matrix.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'; @@ -12,6 +6,11 @@ import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; + import '../../../config/app_config.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 04be9b485..549b8afa5 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -1,20 +1,18 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/models/span_card_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/models/span_card_model.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import '../constants/model_keys.dart'; -import '../utils/overlay.dart'; import '../widgets/igc/span_card.dart'; -import '../widgets/igc/word_data_card.dart'; import 'language_detection_model.dart'; // import 'package:language_tool/language_tool.dart'; @@ -150,35 +148,30 @@ class IGCTextData { } } - int tokenIndexByOffset( - cursorOffset, - ) => - tokens.indexWhere( + int tokenIndexByOffset(cursorOffset) => tokens.indexWhere( (token) => - token.text.offset <= cursorOffset && - cursorOffset <= token.text.offset + token.text.length, + token.text.offset <= cursorOffset && cursorOffset <= token.end, ); - List getMatchIndicesForToken(PangeaToken token) => - matchIndicesByOffset(token.text.offset); + List matchIndicesByOffset(int offset) { + final List matchesForOffset = []; + for (final (index, match) in matches.indexed) { + if (match.isOffsetInMatchSpan(offset)) { + matchesForOffset.add(index); + } + } + return matchesForOffset; + } int getTopMatchIndexForOffset(int offset) { final List matchesForToken = matchIndicesByOffset(offset); - if (matchesForToken.isEmpty) return -1; - for (final matchIndex in matchesForToken) { + final int matchIndex = matchesForToken.indexWhere((matchIndex) { final match = matches[matchIndex]; - if (enableIT) { - if (match.isITStart || match.isl1SpanMatch) { - return matchIndex; - } - } - if (enableIGC) { - if (match.isGrammarMatch) { - return matchIndex; - } - } - } - return -1; + return (enableIT && (match.isITStart || match.isl1SpanMatch)) || + (enableIGC && match.isGrammarMatch); + }); + if (matchIndex == -1) return -1; + return matchesForToken[matchIndex]; } PangeaMatch? getTopMatchForToken(PangeaToken token) { @@ -187,23 +180,8 @@ class IGCTextData { return matches[topMatchIndex]; } - List matchIndicesByOffset(int offset) { - final List matchesForOffset = []; - - for (final (index, match) in matches.indexed) { - if (match.isOffsetInMatchSpan(offset)) { - matchesForOffset.add(index); - } - } - - return matchesForOffset; - } - - int getAfterTokenSpacingByIndex( - int tokenIndex, - ) { - final int endOfToken = - tokens[tokenIndex].text.offset + tokens[tokenIndex].text.length; + int getAfterTokenSpacingByIndex(int tokenIndex) { + final int endOfToken = tokens[tokenIndex].end; if (tokenIndex + 1 < tokens.length) { final spaceBetween = tokens[tokenIndex + 1].text.offset - endOfToken; @@ -218,7 +196,7 @@ class IGCTextData { ), ); ErrorHandler.logError( - m: "wierd token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", + m: "weird token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", ); return 0; } @@ -234,20 +212,42 @@ class IGCTextData { decorationThickness: 5, ); - static const _hasDefinitionStyle = TextStyle( - decoration: TextDecoration.underline, - decorationColor: Color.fromARGB(148, 83, 97, 255), - decorationThickness: 4, - ); - static TextStyle hasDefinitionStyle(TextStyle? existingStyle) => - existingStyle?.merge(_hasDefinitionStyle) ?? _hasDefinitionStyle; + List getMatchTokens() { + final List matchTokens = []; + int? endTokenIndex; + PangeaMatch? topMatch; + for (final (i, token) in tokens.indexed) { + if (endTokenIndex != null) { + if (i <= endTokenIndex) { + matchTokens.add( + MatchToken( + token: token, + match: topMatch, + ), + ); + continue; + } + endTokenIndex = null; + } + topMatch = getTopMatchForToken(token); + if (topMatch != null) { + endTokenIndex = tokens.indexWhere((e) => e.end >= topMatch!.end, i); + } + matchTokens.add( + MatchToken( + token: token, + match: topMatch, + ), + ); + } + return matchTokens; + } //PTODO - handle multitoken spans List constructTokenSpan({ required BuildContext context, TextStyle? defaultStyle, required SpanCardModel? spanCardModel, - required bool showTokens, required bool handleClick, required String transformTargetId, required Room room, @@ -263,73 +263,77 @@ class IGCTextData { ]; } - // or could make big strings for non-match text and therefore make less textspans. - // would that be more performant? - tokens.asMap().forEach( - (index, token) { - final PangeaMatch? topTokenMatch = getTopMatchForToken( - tokens[index], - ); - // if (index == 3) { - // debugPrint( - // "constructing span with topTokenMatch: ${topTokenMatch?.match.rule.id}"); - // } - - final Widget cardToShow = spanCardModel != null && topTokenMatch != null - ? SpanCard( - scm: spanCardModel, - ) - : WordDataCard( - fullText: originalInput, - fullTextLang: detections.first.langCode, - word: token.text.content, - wordLang: detections.first.langCode, - hasInfo: token.hasInfo, - room: room, - ); - - final TextStyle tokenStyle = topTokenMatch != null - ? topTokenMatch.textStyle(defaultStyle) - : hasDefinitionStyle(defaultStyle); + final List matchTokens = getMatchTokens(); + + for (int tokenIndex = 0; tokenIndex < matchTokens.length; tokenIndex++) { + final MatchToken matchToken = matchTokens[tokenIndex]; + final Widget? cardToShow = + matchToken.match != null && spanCardModel != null + ? SpanCard(scm: spanCardModel) + : null; + + int nextTokenIndex = matchTokens.indexWhere( + (e) => matchToken.match != null + ? e.match != matchToken.match + : e.match != null, + tokenIndex, + ); + + if (nextTokenIndex < 0) { + nextTokenIndex = matchTokens.length; + } + + final String matchText = originalInput.substring( + matchTokens[tokenIndex].token.text.offset, + matchTokens[nextTokenIndex - 1].token.end, + ); + items.add( + TextSpan( + text: matchText, + style: matchTokens[tokenIndex].match?.textStyle(defaultStyle) ?? + defaultStyle, + recognizer: handleClick && cardToShow != null + ? (TapGestureRecognizer() + ..onTapDown = (details) => OverlayUtil.showPositionedCard( + context: context, + cardToShow: cardToShow, + cardSize: + matchTokens[tokenIndex].match?.isITStart ?? false + ? const Size(350, 220) + : const Size(350, 400), + transformTargetId: transformTargetId, + )) + : null, + ), + ); + + final String beforeNextToken = originalInput.substring( + matchTokens[nextTokenIndex - 1].token.end, + nextTokenIndex < matchTokens.length + ? matchTokens[nextTokenIndex].token.text.offset + : originalInput.length, + ); + + if (beforeNextToken.isNotEmpty) { items.add( TextSpan( - text: token.text.content, - style: tokenStyle, - recognizer: handleClick - ? (TapGestureRecognizer() - ..onTapDown = (details) => OverlayUtil.showPositionedCard( - context: context, - cardToShow: cardToShow, - cardSize: topTokenMatch?.isITStart ?? false - ? const Size(350, 220) - : const Size(350, 400), - transformTargetId: transformTargetId, - )) - : null, + text: beforeNextToken, + style: defaultStyle, ), ); + } - final int charBetween = getAfterTokenSpacingByIndex( - index, - ); - - if (charBetween > 0) { - items.add( - TextSpan( - text: " " * charBetween, - style: topTokenMatch != null && - token.text.offset + token.text.length + charBetween <= - topTokenMatch.match.offset + - topTokenMatch.match.length - ? tokenStyle - : defaultStyle, - ), - ); - } - }, - ); + tokenIndex = nextTokenIndex - 1; + } return items; } } + +class MatchToken { + final PangeaToken token; + final PangeaMatch? match; + + MatchToken({required this.token, this.match}); +} diff --git a/lib/pangea/models/pangea_match_model.dart b/lib/pangea/models/pangea_match_model.dart index c7e97f4c0..827816890 100644 --- a/lib/pangea/models/pangea_match_model.dart +++ b/lib/pangea/models/pangea_match_model.dart @@ -1,10 +1,10 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/enum/span_data_type.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import '../constants/match_rule_ids.dart'; import 'igc_text_data_model.dart'; import 'span_data.dart'; @@ -127,4 +127,9 @@ class PangeaMatch { IGCTextData.underlineStyle(underlineColor); PangeaMatch get copyWith => PangeaMatch.fromJson(toJson()); + + int get beginning => match.offset < 0 ? 0 : match.offset; + int get end => match.offset + match.length > match.fullText.length + ? match.fullText.length + : match.offset + match.length; } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 217cdc55b..19eaba750 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; - import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/model_keys.dart'; @@ -65,6 +64,8 @@ class PangeaToken { _hasInfoKey: hasInfo, _lemmaKey: lemmas.map((e) => e.toJson()).toList(), }; + + int get end => text.offset + text.length; } class PangeaTokenText { diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index 46efff974..e7ee11451 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; - import 'package:sentry_flutter/sentry_flutter.dart'; import '../models/widget_measurement.dart'; @@ -38,6 +37,12 @@ class PangeaAnyState { _layerLinkAndKeys.remove(transformTargetId); } + void openOverlay(OverlayEntry entry, BuildContext context) { + closeOverlay(); + overlay = entry; + Overlay.of(context).insert(overlay!); + } + void closeOverlay() { if (overlay != null) { overlay!.remove(); diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 33dea2018..2d8c465eb 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -1,64 +1,101 @@ import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import '../../config/themes.dart'; import '../../widgets/matrix.dart'; import 'error_handler.dart'; class OverlayUtil { - static showPositionedCard({ + static showOverlay({ required BuildContext context, - required Widget cardToShow, - required Size cardSize, + required Widget child, + required Size size, required String transformTargetId, + Offset? offset, backDropToDismiss = true, Color? borderColor, }) { try { MatrixState.pAnyState.closeOverlay(); - final LayerLinkAndKey layerLinkAndKey = MatrixState.pAnyState.layerLinkAndKey(transformTargetId); - final Offset cardOffset = _calculateCardOffset( - cardSize: cardSize, - transformTargetKey: layerLinkAndKey.key, - ); - - MatrixState.pAnyState.overlay = OverlayEntry( + 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(); + }, + ), + ), if (backDropToDismiss) const TransparentBackdrop(), Positioned( - width: cardSize.width, - height: cardSize.height, + width: size.width, + height: size.height, child: CompositedTransformFollower( link: layerLinkAndKey.link, showWhenUnlinked: false, - offset: cardOffset, - child: Material( - borderOnForeground: false, - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: OverlayContainer( - cardToShow: cardToShow, - borderColor: borderColor, - ), - ), + offset: offset ?? Offset.zero, + child: child, ), ), ], ), ); - Overlay.of(layerLinkAndKey.key.currentContext!) - .insert(MatrixState.pAnyState.overlay!); + MatrixState.pAnyState.openOverlay(entry, context); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + static showPositionedCard({ + required BuildContext context, + required Widget cardToShow, + required Size cardSize, + required String transformTargetId, + backDropToDismiss = true, + Color? borderColor, + }) { + try { + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(transformTargetId); + + final Offset cardOffset = _calculateCardOffset( + cardSize: cardSize, + transformTargetKey: layerLinkAndKey.key, + ); + + final Widget child = Material( + borderOnForeground: false, + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: OverlayContainer( + cardToShow: cardToShow, + borderColor: borderColor, + ), + ); + + showOverlay( + context: context, + child: child, + size: cardSize, + transformTargetId: transformTargetId, + offset: cardOffset, + backDropToDismiss: backDropToDismiss, + borderColor: borderColor, + ); } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); @@ -132,6 +169,8 @@ class OverlayUtil { return Offset(dx, dy); } + + static bool get isOverlayOpen => MatrixState.pAnyState.overlay != null; } class TransparentBackdrop extends StatelessWidget { diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart new file mode 100644 index 000000000..000251102 --- /dev/null +++ b/lib/pangea/utils/show_defintion_util.dart @@ -0,0 +1,130 @@ +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/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ShowDefintionUtil { + final String messageText; + final String langCode; + final String targetId; + final FocusNode focusNode = FocusNode(); + final Room room; + TextSelection? textSelection; + bool inCooldown = false; + + ShowDefintionUtil({ + required this.targetId, + required this.room, + required this.langCode, + required this.messageText, + }); + + void onTextSelection( + TextSelection selection, + SelectionChangedCause? cause, + BuildContext context, + ) { + selection.isCollapsed + ? clearTextSelection() + : setTextSelection( + selection, + cause, + context, + ); + } + + void setTextSelection( + TextSelection selection, + SelectionChangedCause? cause, + BuildContext context, + ) { + textSelection = selection; + 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) { + final String? fullText = textSelection?.textInside(messageText); + if (fullText == null) return; + OverlayUtil.showPositionedCard( + context: context, + cardToShow: WordDataCard( + word: fullText, + 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 RenderBox? targetRenderBox = + (layerLinkAndKey.key.currentContext!.findRenderObject() as RenderBox?); + final Size? transformTargetSize = targetRenderBox?.size; + + Offset? transformTargetOffset; + if (transformTargetSize != null) { + transformTargetOffset = Offset( + (transformTargetSize.width / 2) - 65, + transformTargetSize.height * -1, + ); + } + + 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: transformTargetOffset, + ); + } +} diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 746c65cc2..781f476bd 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,11 +1,7 @@ +import 'dart:async'; import 'dart:developer'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/constants/language_keys.dart'; @@ -13,12 +9,17 @@ 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/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../../models/igc_text_data_model.dart'; import '../../models/language_detection_model.dart'; import '../../models/pangea_match_model.dart'; import '../../models/pangea_representation_event.dart'; -import '../../utils/bot_style.dart'; import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { @@ -51,6 +52,7 @@ class PangeaRichTextState extends State { bool _fetchingTokens = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; List textSpan = []; + ShowDefintionUtil? messageToolbar; @override void initState() { @@ -68,6 +70,14 @@ class PangeaRichTextState extends State { Widget build(BuildContext context) { //TODO - take out of build function of every message // if (areLanguagesSet) { + messageToolbar = ShowDefintionUtil( + targetId: widget.pangeaMessageEvent.eventId, + room: widget.pangeaMessageEvent.room, + langCode: widget.selectedDisplayLang?.langCode ?? + userL2LangCode ?? + LanguageKeys.unknownLanguage, + messageText: textSpan.map((x) => x.text).join(), + ); if (!widget.selected && widget.selectedDisplayLang != null && @@ -85,29 +95,51 @@ class PangeaRichTextState extends State { ); } - final Widget richText = RichText( - text: TextSpan( - children: [ - ...textSpan, - if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) - // if (widget.selected) - 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 TextSpan richTextSpan = TextSpan( + children: [ + ...textSpan, + if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) + 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 = widget.selected + ? SelectableText.rich( + richTextSpan, + onSelectionChanged: (selection, cause) => kIsWeb + ? messageToolbar?.onTextSelection(selection, cause, context) + : null, + focusNode: messageToolbar?.focusNode, + contextMenuBuilder: (context, selection) { + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: selection.contextMenuAnchors, + buttonItems: [ + ...selection.contextMenuButtonItems, + ContextMenuButtonItem( + label: L10n.of(context)!.showDefinition, + onPressed: () { + messageToolbar?.showDefinition(context); + messageToolbar?.focusNode.unfocus(); + }, + ), + ], + ); + }, + ) + : RichText(text: richTextSpan); + return blur > 0 ? ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), @@ -190,10 +222,9 @@ class PangeaRichTextState extends State { userL1: userL1LangCode ?? LanguageKeys.unknownLanguage, ).constructTokenSpan( context: context, - defaultStyle: textStyle(repEvent, context), + defaultStyle: widget.existingStyle, handleClick: true, spanCardModel: null, - showTokens: widget.definitions, transformTargetId: widget.pangeaMessageEvent.eventId, room: widget.pangeaMessageEvent.room, ); @@ -213,20 +244,10 @@ class PangeaRichTextState extends State { [ TextSpan( text: repEvent.text, - style: textStyle(repEvent, context), + style: widget.existingStyle, ), ]; - TextStyle? textStyle(RepresentationEvent repEvent, BuildContext context) => - // !repEvent.botAuthored - true - ? widget.existingStyle - : BotStyle.text( - context, - existingStyle: widget.existingStyle, - setColor: false, - ); - bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; @@ -258,4 +279,75 @@ class PangeaRichTextState extends State { Future onSentenceRewrite(String sentenceRewrite) async { debugPrint("PTODO implement onSentenceRewrite"); } + + // void onTextSelection( + // TextSelection selection, + // SelectionChangedCause? _, + // ) => + // selection.isCollapsed + // ? clearTextSelection() + // : setTextSelection(selection); + + // void setTextSelection(TextSelection selection) { + // textSelection = selection; + // if (BrowserContextMenu.enabled && kIsWeb) { + // BrowserContextMenu.disableContextMenu(); + // } + // kIsWeb ? showToolbar() : showDefinition(); + // } + + // void clearTextSelection() { + // textSelection = null; + // if (kIsWeb && !BrowserContextMenu.enabled) { + // BrowserContextMenu.enableContextMenu(); + // } + // } + + // void showToolbar() async { + // if (toolbarShowing || !kIsWeb) return; + // toolbarShowing = true; + // await Future.delayed(const Duration(seconds: 2)); + + // final toolbarFuture = MessageToolbar.showToolbar( + // context, + // widget.pangeaMessageEvent.eventId, + // _focusNode.offset, + // ); + + // final resp = await toolbarFuture; + // toolbarShowing = false; + + // switch (resp) { + // case null: + // break; + // case 1: + // showDefinition(); + // break; + // default: + // break; + // } + // } + + // void showDefinition() { + // final String messageText = textSpan.map((x) => x.text).join(); + // final String fullText = textSelection!.textInside(messageText); + // final String langCode = widget.selectedDisplayLang?.langCode ?? + // userL2LangCode ?? + // LanguageKeys.unknownLanguage; + + // OverlayUtil.showPositionedCard( + // context: context, + // cardToShow: WordDataCard( + // word: fullText, + // wordLang: langCode, + // fullText: messageText, + // fullTextLang: langCode, + // hasInfo: false, + // room: widget.pangeaMessageEvent.room, + // ), + // cardSize: const Size(300, 300), + // transformTargetId: widget.pangeaMessageEvent.eventId, + // backDropToDismiss: false, + // ); + // } } diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index 5e64b4bef..886274316 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -1,13 +1,11 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; -import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import '../../choreographer/controllers/choreographer.dart'; import '../../enum/edit_type.dart'; -import '../../models/pangea_token_model.dart'; import '../../models/span_card_model.dart'; import '../../models/widget_measurement.dart'; import '../../utils/overlay.dart'; @@ -53,12 +51,11 @@ class PangeaTextController extends TextEditingController { if (tokenIndex == -1) return; - final PangeaToken token = choreographer.igc.igcTextData!.tokens[tokenIndex]; final int matchIndex = choreographer.igc.igcTextData!.getTopMatchIndexForOffset( selection.baseOffset, ); - final Widget cardToShow = matchIndex != -1 + final Widget? cardToShow = matchIndex != -1 ? SpanCard( scm: SpanCardModel( // igcTextData: choreographer.igc.igcTextData!, @@ -80,27 +77,19 @@ class PangeaTextController extends TextEditingController { ), roomId: choreographer.roomId, ) - : WordDataCard( - fullText: text, - fullTextLang: - choreographer.igc.igcTextData!.detections.first.langCode, - word: token.text.content, - //Note: this assumes that the token must be in the target language - //since it didn't have a match - wordLang: choreographer.itController.targetLangCode, - hasInfo: token.hasInfo, - room: choreographer.chatController.room, - ); - - OverlayUtil.showPositionedCard( - context: context, - cardSize: matchIndex != -1 && - choreographer.igc.igcTextData!.matches[matchIndex].isITStart - ? const Size(350, 220) - : const Size(350, 400), - cardToShow: cardToShow, - transformTargetId: choreographer.inputTransformTargetKey, - ); + : null; + + if (cardToShow != null) { + OverlayUtil.showPositionedCard( + context: context, + cardSize: matchIndex != -1 && + choreographer.igc.igcTextData!.matches[matchIndex].isITStart + ? const Size(350, 220) + : const Size(350, 400), + cardToShow: cardToShow, + transformTargetId: choreographer.inputTransformTargetKey, + ); + } } @override @@ -139,7 +128,6 @@ class PangeaTextController extends TextEditingController { ...choreographer.igc.igcTextData!.constructTokenSpan( context: context, defaultStyle: style, - showTokens: choreographer.definitionsEnabled, spanCardModel: null, handleClick: false, transformTargetId: choreographer.inputTransformTargetKey, diff --git a/needed-translations.txt b/needed-translations.txt index cdb97658e..becd1bd8f 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -765,7 +765,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "bn": [ @@ -1539,7 +1540,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "bo": [ @@ -2313,7 +2315,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ca": [ @@ -3082,7 +3085,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "cs": [ @@ -3851,7 +3855,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "de": [ @@ -4620,7 +4625,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "el": [ @@ -5394,7 +5400,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "eo": [ @@ -6163,7 +6170,12 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" + ], + + "es": [ + "showDefinition" ], "et": [ @@ -6932,7 +6944,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "eu": [ @@ -7701,7 +7714,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fa": [ @@ -8470,7 +8484,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fi": [ @@ -9239,7 +9254,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fr": [ @@ -10008,7 +10024,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ga": [ @@ -10777,7 +10794,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "gl": [ @@ -11546,7 +11564,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "he": [ @@ -12315,7 +12334,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hi": [ @@ -13089,7 +13109,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hr": [ @@ -13858,7 +13879,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hu": [ @@ -14627,7 +14649,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "id": [ @@ -15396,7 +15419,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ie": [ @@ -16167,7 +16191,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "it": [ @@ -16936,7 +16961,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ja": [ @@ -17705,7 +17731,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ko": [ @@ -18474,7 +18501,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "lt": [ @@ -19243,7 +19271,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "lv": [ @@ -20017,7 +20046,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "nb": [ @@ -20786,7 +20816,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "nl": [ @@ -21555,7 +21586,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pl": [ @@ -22324,7 +22356,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt": [ @@ -23098,7 +23131,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt_BR": [ @@ -23867,7 +23901,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt_PT": [ @@ -24636,7 +24671,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ro": [ @@ -25405,7 +25441,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ru": [ @@ -26174,7 +26211,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sk": [ @@ -26944,7 +26982,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sl": [ @@ -27716,7 +27755,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sr": [ @@ -28485,7 +28525,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sv": [ @@ -29254,7 +29295,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ta": [ @@ -30028,7 +30070,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "th": [ @@ -30802,7 +30845,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "tr": [ @@ -31571,7 +31615,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "uk": [ @@ -32340,7 +32385,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "vi": [ @@ -33112,7 +33158,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "zh": [ @@ -33881,7 +33928,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "zh_Hant": [ @@ -34650,6 +34698,7 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ] } diff --git a/pubspec.yaml b/pubspec.yaml index 926c61523..0b4b59916 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,6 +138,7 @@ flutter: generate: true uses-material-design: true assets: + - .env - assets/ # #Pangea - assets/pangea/ From ad16c6dfef804b141c55892885b999f3da315eba Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 23 Jan 2024 09:25:33 -0500 Subject: [PATCH 2/4] re-integrated select defintions with new fluffychat merge --- lib/pages/chat/chat.dart | 6 - lib/pages/chat/events/message_content.dart | 120 ++++++++----- lib/pangea/utils/instructions.dart | 6 - lib/pangea/utils/show_defintion_util.dart | 46 +++-- lib/pangea/widgets/igc/pangea_rich_text.dart | 174 +++++-------------- 5 files changed, 159 insertions(+), 193 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d499a5b82..f31568064 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -23,7 +23,6 @@ import 'package:fluffychat/pangea/models/message_data_models.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/instructions.dart'; import 'package:fluffychat/pangea/utils/report_message.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -1282,11 +1281,6 @@ class ChatController extends State if (choreographer.itController.isOpen) { return; } - pangeaController.instructions.show( - context, - InstructionsEnum.understandingMessages, - event.eventId, - ); // Pangea# if (!event.redacted) { if (selectedEvents.contains(event)) { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index d719d7652..c8d5b886d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,11 +1,13 @@ 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/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -36,9 +38,10 @@ class MessageContent extends StatelessWidget { final LanguageModel? selectedDisplayLang; final bool immersionMode; final bool definitions; + ShowDefintionUtil? messageToolbar; // Pangea# - const MessageContent( + MessageContent( this.event, { this.onInfoTab, super.key, @@ -121,6 +124,18 @@ 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) { @@ -263,55 +278,74 @@ class MessageContent extends StatelessWidget { height: 1.3, ); if (pangeaMessageEvent.showRichText) { - return PangeaRichText( - existingStyle: messageTextStyle, - selected: selected, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - definitions: definitions, - selectedDisplayLang: selectedDisplayLang, + return MouseRegion( + onHover: messageToolbar?.onMouseRegionUpdate, + child: PangeaRichText( + existingStyle: messageTextStyle, + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: immersionMode, + definitions: definitions, + selectedDisplayLang: selectedDisplayLang, + messageToolbar: messageToolbar, + ), ); } - //Pangea# - return FutureBuilder( - 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 MouseRegion( + onHover: messageToolbar?.onMouseRegionUpdate, + child: FutureBuilder( // Pangea# - return Linkify( - text: snapshot.data ?? + 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: 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(), - ); - }, + 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: messageToolbar?.contextMenuOverride, + // 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(), + onSelectionChanged: (selection, cause) => messageToolbar + ?.onTextSelection(selection, cause, context), + ); + }, + ), ); } case EventTypes.CallInvite: diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 40253a00d..39b1d0c26 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../config/app_config.dart'; @@ -96,7 +95,6 @@ class InstructionsController { enum InstructionsEnum { itInstructions, clickMessage, - understandingMessages, blurMeansTranslate, } @@ -107,8 +105,6 @@ extension Copy on InstructionsEnum { return L10n.of(context)!.itInstructionsTitle; case InstructionsEnum.clickMessage: return L10n.of(context)!.clickMessageTitle; - case InstructionsEnum.understandingMessages: - return L10n.of(context)!.understandingMessagesTitle; case InstructionsEnum.blurMeansTranslate: return L10n.of(context)!.blurMeansTranslateTitle; } @@ -120,8 +116,6 @@ extension Copy on InstructionsEnum { return L10n.of(context)!.itInstructionsBody; case InstructionsEnum.clickMessage: return L10n.of(context)!.clickMessageBody; - case InstructionsEnum.understandingMessages: - return L10n.of(context)!.understandingMessagesBody; case InstructionsEnum.blurMeansTranslate: return L10n.of(context)!.blurMeansTranslateBody; } diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart index 000251102..59a095eed 100644 --- a/lib/pangea/utils/show_defintion_util.dart +++ b/lib/pangea/utils/show_defintion_util.dart @@ -11,13 +11,15 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class ShowDefintionUtil { - final String messageText; + String messageText; final String langCode; final String targetId; final FocusNode focusNode = FocusNode(); final Room room; TextSelection? textSelection; bool inCooldown = false; + double? dx; + double? dy; ShowDefintionUtil({ required this.targetId, @@ -93,17 +95,18 @@ class ShowDefintionUtil { Future showToolbar(BuildContext context) async { final LayerLinkAndKey layerLinkAndKey = MatrixState.pAnyState.layerLinkAndKey(targetId); - final RenderBox? targetRenderBox = - (layerLinkAndKey.key.currentContext!.findRenderObject() as RenderBox?); - final Size? transformTargetSize = targetRenderBox?.size; - Offset? transformTargetOffset; - if (transformTargetSize != null) { - transformTargetOffset = Offset( - (transformTargetSize.width / 2) - 65, - transformTargetSize.height * -1, - ); + 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, @@ -124,7 +127,28 @@ class ShowDefintionUtil { ), size: const Size(130, 45), transformTargetId: targetId, - offset: transformTargetOffset, + offset: Offset(xOffset, yOffset), + ); + } + + void onMouseRegionUpdate(PointerEvent event) { + dx = event.position.dx; + dy = event.position.dy; + } + + Widget contextMenuOverride(BuildContext context, EditableTextState selection) { + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: selection.contextMenuAnchors, + buttonItems: [ + ...selection.contextMenuButtonItems, + ContextMenuButtonItem( + label: L10n.of(context)!.showDefinition, + onPressed: () { + showDefinition(context); + focusNode.unfocus(); + }, + ), + ], ); } } diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 781f476bd..65810c572 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:developer'; import 'dart:ui'; @@ -13,13 +12,13 @@ import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../models/igc_text_data_model.dart'; import '../../models/language_detection_model.dart'; import '../../models/pangea_match_model.dart'; import '../../models/pangea_representation_event.dart'; +import '../../utils/bot_style.dart'; import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { @@ -30,6 +29,7 @@ class PangeaRichText extends StatefulWidget { final bool immersionMode; final bool definitions; final Choreographer? choreographer; + final ShowDefintionUtil? messageToolbar; const PangeaRichText({ super.key, @@ -40,6 +40,7 @@ class PangeaRichText extends StatefulWidget { required this.definitions, this.choreographer, this.existingStyle, + this.messageToolbar, }); @override @@ -52,32 +53,30 @@ class PangeaRichTextState extends State { bool _fetchingTokens = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; List textSpan = []; - ShowDefintionUtil? messageToolbar; @override void initState() { super.initState(); - setState(() => textSpan = getTextSpan(context)); + updateTextSpan(); } @override void didUpdateWidget(PangeaRichText oldWidget) { super.didUpdateWidget(oldWidget); - setState(() => textSpan = getTextSpan(context)); + updateTextSpan(); + } + + void updateTextSpan() { + setState(() { + textSpan = getTextSpan(context); + widget.messageToolbar?.messageText = textSpan.map((e) => e.text).join(); + }); } @override Widget build(BuildContext context) { //TODO - take out of build function of every message // if (areLanguagesSet) { - messageToolbar = ShowDefintionUtil( - targetId: widget.pangeaMessageEvent.eventId, - room: widget.pangeaMessageEvent.room, - langCode: widget.selectedDisplayLang?.langCode ?? - userL2LangCode ?? - LanguageKeys.unknownLanguage, - messageText: textSpan.map((x) => x.text).join(), - ); if (!widget.selected && widget.selectedDisplayLang != null && @@ -95,51 +94,33 @@ class PangeaRichTextState extends State { ); } - final TextSpan richTextSpan = TextSpan( - children: [ - ...textSpan, - if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) - 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.messageToolbar?.onTextSelection(selection, cause, context), + focusNode: widget.messageToolbar?.focusNode, + contextMenuBuilder: widget.messageToolbar?.contextMenuOverride, + TextSpan( + children: [ + ...textSpan, + if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) + // if (widget.selected) + 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 = widget.selected - ? SelectableText.rich( - richTextSpan, - onSelectionChanged: (selection, cause) => kIsWeb - ? messageToolbar?.onTextSelection(selection, cause, context) - : null, - focusNode: messageToolbar?.focusNode, - contextMenuBuilder: (context, selection) { - return AdaptiveTextSelectionToolbar.buttonItems( - anchors: selection.contextMenuAnchors, - buttonItems: [ - ...selection.contextMenuButtonItems, - ContextMenuButtonItem( - label: L10n.of(context)!.showDefinition, - onPressed: () { - messageToolbar?.showDefinition(context); - messageToolbar?.focusNode.unfocus(); - }, - ), - ], - ); - }, - ) - : RichText(text: richTextSpan); - return blur > 0 ? ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), @@ -222,8 +203,8 @@ class PangeaRichTextState extends State { userL1: userL1LangCode ?? LanguageKeys.unknownLanguage, ).constructTokenSpan( context: context, - defaultStyle: widget.existingStyle, - handleClick: true, + defaultStyle: textStyle(repEvent, context), + handleClick: false, spanCardModel: null, transformTargetId: widget.pangeaMessageEvent.eventId, room: widget.pangeaMessageEvent.room, @@ -244,10 +225,20 @@ class PangeaRichTextState extends State { [ TextSpan( text: repEvent.text, - style: widget.existingStyle, + style: textStyle(repEvent, context), ), ]; + TextStyle? textStyle(RepresentationEvent repEvent, BuildContext context) => + // !repEvent.botAuthored + true + ? widget.existingStyle + : BotStyle.text( + context, + existingStyle: widget.existingStyle, + setColor: false, + ); + bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; @@ -279,75 +270,4 @@ class PangeaRichTextState extends State { Future onSentenceRewrite(String sentenceRewrite) async { debugPrint("PTODO implement onSentenceRewrite"); } - - // void onTextSelection( - // TextSelection selection, - // SelectionChangedCause? _, - // ) => - // selection.isCollapsed - // ? clearTextSelection() - // : setTextSelection(selection); - - // void setTextSelection(TextSelection selection) { - // textSelection = selection; - // if (BrowserContextMenu.enabled && kIsWeb) { - // BrowserContextMenu.disableContextMenu(); - // } - // kIsWeb ? showToolbar() : showDefinition(); - // } - - // void clearTextSelection() { - // textSelection = null; - // if (kIsWeb && !BrowserContextMenu.enabled) { - // BrowserContextMenu.enableContextMenu(); - // } - // } - - // void showToolbar() async { - // if (toolbarShowing || !kIsWeb) return; - // toolbarShowing = true; - // await Future.delayed(const Duration(seconds: 2)); - - // final toolbarFuture = MessageToolbar.showToolbar( - // context, - // widget.pangeaMessageEvent.eventId, - // _focusNode.offset, - // ); - - // final resp = await toolbarFuture; - // toolbarShowing = false; - - // switch (resp) { - // case null: - // break; - // case 1: - // showDefinition(); - // break; - // default: - // break; - // } - // } - - // void showDefinition() { - // final String messageText = textSpan.map((x) => x.text).join(); - // final String fullText = textSelection!.textInside(messageText); - // final String langCode = widget.selectedDisplayLang?.langCode ?? - // userL2LangCode ?? - // LanguageKeys.unknownLanguage; - - // OverlayUtil.showPositionedCard( - // context: context, - // cardToShow: WordDataCard( - // word: fullText, - // wordLang: langCode, - // fullText: messageText, - // fullTextLang: langCode, - // hasInfo: false, - // room: widget.pangeaMessageEvent.room, - // ), - // cardSize: const Size(300, 300), - // transformTargetId: widget.pangeaMessageEvent.eventId, - // backDropToDismiss: false, - // ); - // } } From 2eb4c04d2ba5fbfb404ab5cbab463c5f341a2b6c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 24 Jan 2024 15:48:03 -0500 Subject: [PATCH 3/4] added selecton to html messages --- lib/config/app_config.dart | 5 +- lib/pages/chat/events/html_message.dart | 191 ++++++++++-------- lib/pages/chat/events/message_content.dart | 32 ++- .../extensions/pangea_room_extension.dart | 29 +-- lib/pangea/models/pangea_message_event.dart | 5 + .../models/pangea_representation_event.dart | 14 +- lib/pangea/utils/show_defintion_util.dart | 55 ++--- lib/pangea/widgets/igc/pangea_rich_text.dart | 152 +++++--------- 8 files changed, 250 insertions(+), 233 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 75293a28e..3136bfd56 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -63,10 +63,7 @@ abstract class AppConfig { static const bool enableSentry = true; static const String sentryDns = 'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143'; - //#Pangea - static bool renderHtml = false; - // static bool renderHtml = true; - //Pangea# + static bool renderHtml = true; static bool hideRedactedEvents = false; static bool hideUnknownEvents = true; static bool hideUnimportantStateEvents = true; diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index ae67c5813..2ea75ab74 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,6 +1,10 @@ -import 'package:flutter/material.dart'; - 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'; @@ -10,21 +14,24 @@ import 'package:html/dom.dart' as dom; import 'package:linkify/linkify.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../utils/url_launcher.dart'; class HtmlMessage extends StatelessWidget { final String html; final Room room; final Color textColor; + // #Pangea + final ShowDefintionUtil? messageToolbar; + // Pangea# const HtmlMessage({ super.key, required this.html, required this.room, this.textColor = Colors.black, + // #Pangea + this.messageToolbar, + // Pangea# }); dom.Node _linkifyHtml(dom.Node element) { @@ -92,84 +99,108 @@ class HtmlMessage extends StatelessWidget { final element = _linkifyHtml(HtmlParser.parseHTML(renderHtml)); // there is no need to pre-validate the html, as we validate it while rendering - return Html.fromElement( - documentElement: element as dom.Element, - style: { - '*': Style( - color: textColor, - margin: Margins.all(0), - fontSize: FontSize(fontSize), - ), - 'a': Style(color: linkColor, textDecorationColor: linkColor), - 'h1': Style( - fontSize: FontSize(fontSize * 2), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w600, - ), - 'h2': Style( - fontSize: FontSize(fontSize * 1.75), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w500, - ), - 'h3': Style( - fontSize: FontSize(fontSize * 1.5), - lineHeight: LineHeight.number(1.5), - ), - 'h4': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h5': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h6': Style( - fontSize: FontSize(fontSize), - lineHeight: LineHeight.number(1.5), - ), - 'blockquote': blockquoteStyle, - 'tg-forward': blockquoteStyle, - 'hr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'table': Style( - border: Border.all(color: textColor, width: 0.5), + // #Pangea + return MouseRegion( + onHover: messageToolbar?.onMouseRegionUpdate, + child: SelectionArea( + onSelectionChanged: (SelectedContent? selection) => + messageToolbar?.onTextSelection( + selectedContent: selection, + context: context, ), - 'tr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'td': Style( - border: Border.all(color: textColor, width: 0.5), - padding: HtmlPaddings.all(2), - ), - 'th': Style( - border: Border.all(color: textColor, width: 0.5), - ), - }, - extensions: [ - RoomPillExtension(context, room), - CodeExtension(fontSize: fontSize), - MatrixMathExtension( - style: TextStyle(fontSize: fontSize, color: textColor), + focusNode: messageToolbar?.focusNode, + contextMenuBuilder: (context, state) => + messageToolbar?.contextMenuOverride( + context: context, + contentSelection: state, + ) ?? + const SizedBox(), + // Pangea# + child: Html.fromElement( + documentElement: element as dom.Element, + style: { + '*': Style( + color: textColor, + margin: Margins.all(0), + fontSize: FontSize(fontSize), + ), + 'a': Style(color: linkColor, textDecorationColor: linkColor), + 'h1': Style( + fontSize: FontSize(fontSize * 2), + lineHeight: LineHeight.number(1.5), + fontWeight: FontWeight.w600, + ), + 'h2': Style( + fontSize: FontSize(fontSize * 1.75), + lineHeight: LineHeight.number(1.5), + fontWeight: FontWeight.w500, + ), + 'h3': Style( + fontSize: FontSize(fontSize * 1.5), + lineHeight: LineHeight.number(1.5), + ), + 'h4': Style( + fontSize: FontSize(fontSize * 1.25), + lineHeight: LineHeight.number(1.5), + ), + 'h5': Style( + fontSize: FontSize(fontSize * 1.25), + lineHeight: LineHeight.number(1.5), + ), + 'h6': Style( + fontSize: FontSize(fontSize), + lineHeight: LineHeight.number(1.5), + ), + 'blockquote': blockquoteStyle, + 'tg-forward': blockquoteStyle, + 'hr': Style( + border: Border.all(color: textColor, width: 0.5), + ), + 'table': Style( + border: Border.all(color: textColor, width: 0.5), + ), + 'tr': Style( + border: Border.all(color: textColor, width: 0.5), + ), + 'td': Style( + border: Border.all(color: textColor, width: 0.5), + padding: HtmlPaddings.all(2), + ), + 'th': Style( + border: Border.all(color: textColor, width: 0.5), + ), + }, + extensions: [ + RoomPillExtension(context, room), + CodeExtension(fontSize: fontSize), + MatrixMathExtension( + style: TextStyle(fontSize: fontSize, color: textColor), + ), + const TableHtmlExtension(), + SpoilerExtension(textColor: textColor), + const ImageExtension(), + FontColorExtension(), + ], + onLinkTap: (url, _, element) => UrlLauncher( + context, + url, + element?.text, + ).launchUrl(), + onlyRenderTheseTags: const { + ...allowedHtmlTags, + // Needed to make it work properly + 'body', + 'html', + }, + shrinkWrap: true, ), - const TableHtmlExtension(), - SpoilerExtension(textColor: textColor), - const ImageExtension(), - FontColorExtension(), - ], - onLinkTap: (url, _, element) => UrlLauncher( - context, - url, - element?.text, - ).launchUrl(), - onlyRenderTheseTags: const { - ...allowedHtmlTags, - // Needed to make it work properly - 'body', - 'html', - }, - shrinkWrap: true, + ), ); + // ), + // ], + // ), + // ), + // ); } /// Keep in sync with: https://spec.matrix.org/v1.6/client-server-api/#mroommessage-msgtypes diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index c8d5b886d..0f6765df2 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,3 +1,4 @@ +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'; @@ -18,7 +19,6 @@ import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; import 'audio_player.dart'; import 'cute_events.dart'; -import 'html_message.dart'; import 'image_bubble.dart'; import 'map_bubble.dart'; import 'message_download_content.dart'; @@ -182,16 +182,25 @@ class MessageContent extends StatelessWidget { case MessageTypes.Notice: case MessageTypes.Emote: if (AppConfig.renderHtml && - !event.redacted && - event.isRichMessage) { + !event.redacted && + event.isRichMessage + // #Pangea + && + !pangeaMessageEvent.showRichText + // Pangea# + ) { var html = event.formattedText; if (event.messageType == MessageTypes.Emote) { html = '* $html'; } + // #Pangea + messageToolbar?.messageText = html; + // Pangea# return HtmlMessage( html: html, textColor: textColor, room: event.room, + messageToolbar: messageToolbar, ); } // else we fall through to the normal message rendering @@ -281,7 +290,7 @@ class MessageContent extends StatelessWidget { return MouseRegion( onHover: messageToolbar?.onMouseRegionUpdate, child: PangeaRichText( - existingStyle: messageTextStyle, + style: messageTextStyle, selected: selected, pangeaMessageEvent: pangeaMessageEvent, immersionMode: immersionMode, @@ -321,7 +330,12 @@ class MessageContent extends StatelessWidget { // Pangea# text: messageText, focusNode: messageToolbar?.focusNode, - contextMenuBuilder: messageToolbar?.contextMenuOverride, + contextMenuBuilder: (context, state) => + messageToolbar?.contextMenuOverride( + context: context, + textSelection: state, + ) ?? + const SizedBox(), // text: snapshot.data ?? // event.calcLocalizedBodyFallback( // MatrixLocals(L10n.of(context)!), @@ -341,8 +355,12 @@ class MessageContent extends StatelessWidget { decorationColor: textColor.withAlpha(150), ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - onSelectionChanged: (selection, cause) => messageToolbar - ?.onTextSelection(selection, cause, context), + onSelectionChanged: (selection, cause) => + messageToolbar?.onTextSelection( + selectedText: selection, + cause: cause, + context: context, + ), ); }, ), diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 27c275484..dd51a06ce 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -10,7 +10,10 @@ import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +// import markdown.dart +import 'package:html_unescape/html_unescape.dart'; import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/markdown.dart'; import 'package:matrix/src/utils/space_child.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -858,7 +861,7 @@ extension PangeaRoom on Room { String? txid, Event? inReplyTo, String? editEventId, - bool parseMarkdown = false, + bool parseMarkdown = true, bool parseCommands = false, String msgtype = MessageTypes.Text, String? threadRootEventId, @@ -888,17 +891,19 @@ extension PangeaRoom on Room { ModelKey.tokensWritten: tokensWritten?.toJson(), ModelKey.useType: useType?.string, }; - // if (parseMarkdown) { - // final html = markdown(event['body'], - // getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), - // getMention: getMention); - // // if the decoded html is the same as the body, there is no need in sending a formatted message - // if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != - // event['body']) { - // event['format'] = 'org.matrix.custom.html'; - // event['formatted_body'] = html; - // } - // } + if (parseMarkdown) { + final html = markdown( + event['body'], + getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), + getMention: getMention, + ); + // if the decoded html is the same as the body, there is no need in sending a formatted message + if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != + event['body']) { + event['format'] = 'org.matrix.custom.html'; + event['formatted_body'] = html; + } + } return sendEvent( event, txid: txid, diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index d515c046f..e9ed46487 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; @@ -256,6 +257,10 @@ class PangeaMessageEvent { //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; } class URLFinder { diff --git a/lib/pangea/models/pangea_representation_event.dart b/lib/pangea/models/pangea_representation_event.dart index 49e0f9358..bc1ed921a 100644 --- a/lib/pangea/models/pangea_representation_event.dart +++ b/lib/pangea/models/pangea_representation_event.dart @@ -1,15 +1,15 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/models/pangea_choreo_event.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/repo/tokens_repo.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -import 'package:fluffychat/pangea/models/pangea_choreo_event.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/repo/tokens_repo.dart'; import '../../widgets/matrix.dart'; import '../constants/language_keys.dart'; import '../constants/pangea_event_types.dart'; @@ -158,4 +158,8 @@ class RepresentationEvent { return _choreo; } + + String? formatBody() { + return markdown(content.text); + } } diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart index 59a095eed..6aa3ba29f 100644 --- a/lib/pangea/utils/show_defintion_util.dart +++ b/lib/pangea/utils/show_defintion_util.dart @@ -6,6 +6,7 @@ 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'; @@ -16,7 +17,7 @@ class ShowDefintionUtil { final String targetId; final FocusNode focusNode = FocusNode(); final Room room; - TextSelection? textSelection; + String? textSelection; bool inCooldown = false; double? dx; double? dy; @@ -28,26 +29,21 @@ class ShowDefintionUtil { required this.messageText, }); - void onTextSelection( - TextSelection selection, + void onTextSelection({ + required BuildContext context, + TextSelection? selectedText, + SelectedContent? selectedContent, SelectionChangedCause? cause, - BuildContext context, - ) { - selection.isCollapsed - ? clearTextSelection() - : setTextSelection( - selection, - cause, - context, - ); - } + }) { + if ((selectedText == null && selectedContent == null) || + selectedText?.isCollapsed == true) { + clearTextSelection(); + return; + } + textSelection = selectedText != null + ? selectedText.textInside(messageText) + : selectedContent!.plainText; - void setTextSelection( - TextSelection selection, - SelectionChangedCause? cause, - BuildContext context, - ) { - textSelection = selection; if (BrowserContextMenu.enabled && kIsWeb) { BrowserContextMenu.disableContextMenu(); } @@ -73,12 +69,11 @@ class ShowDefintionUtil { } void showDefinition(BuildContext context) { - final String? fullText = textSelection?.textInside(messageText); - if (fullText == null) return; + if (textSelection == null) return; OverlayUtil.showPositionedCard( context: context, cardToShow: WordDataCard( - word: fullText, + word: textSelection!, wordLang: langCode, fullText: messageText, fullTextLang: langCode, @@ -136,11 +131,21 @@ class ShowDefintionUtil { dy = event.position.dy; } - Widget contextMenuOverride(BuildContext context, EditableTextState selection) { + Widget contextMenuOverride({ + required BuildContext context, + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + }) { + if (textSelection == null && contentSelection == null) { + return const SizedBox(); + } return AdaptiveTextSelectionToolbar.buttonItems( - anchors: selection.contextMenuAnchors, + anchors: textSelection?.contextMenuAnchors ?? + contentSelection!.contextMenuAnchors, buttonItems: [ - ...selection.contextMenuButtonItems, + if (textSelection != null) ...textSelection.contextMenuButtonItems, + if (contentSelection != null) + ...contentSelection.contextMenuButtonItems, ContextMenuButtonItem( label: L10n.of(context)!.showDefinition, onPressed: () { diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 65810c572..782af3d8b 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -2,6 +2,7 @@ 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'; @@ -14,16 +15,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../models/igc_text_data_model.dart'; -import '../../models/language_detection_model.dart'; import '../../models/pangea_match_model.dart'; import '../../models/pangea_representation_event.dart'; -import '../../utils/bot_style.dart'; import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; - final TextStyle? existingStyle; + final TextStyle? style; final bool selected; final LanguageModel? selectedDisplayLang; final bool immersionMode; @@ -39,7 +37,7 @@ class PangeaRichText extends StatefulWidget { required this.immersionMode, required this.definitions, this.choreographer, - this.existingStyle, + this.style, this.messageToolbar, }); @@ -50,9 +48,8 @@ class PangeaRichText extends StatefulWidget { class PangeaRichTextState extends State { final PangeaController pangeaController = MatrixState.pangeaController; bool _fetchingRepresentation = false; - bool _fetchingTokens = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; - List textSpan = []; + String textSpan = ""; @override void initState() { @@ -69,7 +66,7 @@ class PangeaRichTextState extends State { void updateTextSpan() { setState(() { textSpan = getTextSpan(context); - widget.messageToolbar?.messageText = textSpan.map((e) => e.text).join(); + widget.messageToolbar?.messageText = textSpan; }); } @@ -94,32 +91,48 @@ class PangeaRichTextState extends State { ); } - final Widget richText = SelectableText.rich( - onSelectionChanged: (selection, cause) => - widget.messageToolbar?.onTextSelection(selection, cause, context), - focusNode: widget.messageToolbar?.focusNode, - contextMenuBuilder: widget.messageToolbar?.contextMenuOverride, - TextSpan( - children: [ - ...textSpan, - if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) - // if (widget.selected) - 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 = 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, + ), + 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, + ), + ), + ), ), - ), - ), + ], ), - ], - ), - ); + ); return blur > 0 ? ImageFiltered( @@ -129,17 +142,17 @@ class PangeaRichTextState extends State { : richText; } - List getTextSpan(BuildContext context) { + String getTextSpan(BuildContext context) { final String? displayLangCode = widget.selected ? widget.selectedDisplayLang?.langCode : userL2LangCode; if (displayLangCode == null || !widget.immersionMode) { - return simpleText(widget.pangeaMessageEvent.body); + return widget.pangeaMessageEvent.body; } if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { debugger(when: kDebugMode); - return simpleText(widget.pangeaMessageEvent.body); + return widget.pangeaMessageEvent.body; } final RepresentationEvent? repEvent = @@ -158,7 +171,7 @@ class PangeaRichTextState extends State { ) .onError((error, stackTrace) => ErrorHandler.logError()) .whenComplete(() => setState(() => _fetchingRepresentation = false)); - return simpleText(widget.pangeaMessageEvent.body); + return widget.pangeaMessageEvent.body; } if (repEvent.event?.eventId.contains("web") ?? false) { @@ -171,74 +184,13 @@ class PangeaRichTextState extends State { "representationByLanguageGlobal returned RepEvent with event ID containing 'web' - ${repEvent.event?.eventId}", ), ); - // debugger(when: kDebugMode); - return textWithBotStyle(repEvent, context); - } - - if (!widget.selected || - displayLangCode != userL2LangCode || - !widget.definitions) { - return textWithBotStyle(repEvent, context); } - if (repEvent.tokens == null) { - setState(() => _fetchingTokens = true); - repEvent - .tokensGlobal(context) - .onError((error, stackTrace) => ErrorHandler.logError()) - .whenComplete(() => setState(() => _fetchingTokens = false)); - - return textWithBotStyle(repEvent, context); - } - - return IGCTextData( - originalInput: repEvent.text, - fullTextCorrection: repEvent.text, - matches: [], - detections: [LanguageDetection(langCode: displayLangCode)], - tokens: repEvent.tokens!, - enableIT: true, - enableIGC: true, - userL2: userL2LangCode ?? LanguageKeys.unknownLanguage, - userL1: userL1LangCode ?? LanguageKeys.unknownLanguage, - ).constructTokenSpan( - context: context, - defaultStyle: textStyle(repEvent, context), - handleClick: false, - spanCardModel: null, - transformTargetId: widget.pangeaMessageEvent.eventId, - room: widget.pangeaMessageEvent.room, - ); + return widget.pangeaMessageEvent.isHtml + ? repEvent.formatBody() ?? repEvent.text + : repEvent.text; } - List simpleText(String text) => [ - TextSpan( - text: text, - style: widget.existingStyle, - ), - ]; - - List textWithBotStyle( - RepresentationEvent repEvent, - BuildContext context, - ) => - [ - TextSpan( - text: repEvent.text, - style: textStyle(repEvent, context), - ), - ]; - - TextStyle? textStyle(RepresentationEvent repEvent, BuildContext context) => - // !repEvent.botAuthored - true - ? widget.existingStyle - : BotStyle.text( - context, - existingStyle: widget.existingStyle, - setColor: false, - ); - bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; From 45b6cc93237aaf95d1248a8e131350714a4e347a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 29 Jan 2024 12:09:45 -0500 Subject: [PATCH 4/4] ios build config file updates --- ios/FluffyChat Share/FluffyChat Share.entitlements | 4 +--- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- .../xcshareddata/xcschemes/Runner.xcscheme | 8 ++------ 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/ios/FluffyChat Share/FluffyChat Share.entitlements b/ios/FluffyChat Share/FluffyChat Share.entitlements index 932f3e01e..2eb7e333a 100644 --- a/ios/FluffyChat Share/FluffyChat Share.entitlements +++ b/ios/FluffyChat Share/FluffyChat Share.entitlements @@ -3,8 +3,6 @@ com.apple.security.application-groups - - group.im.fluffychat.app - + diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d245..8c6e56146 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e5c2d6a2b..73d46da15 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -457,7 +457,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -546,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -595,7 +595,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e698..a6b826db2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - -