diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fa1a9f64fd..8210c1949b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4814,5 +4814,6 @@ "appWantsToUseForLoginDescription": "You hereby allow the app and website to share information about you.", "open": "Open", "waitingForServer": "Waiting for server...", - "appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*." + "appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*.", + "whatIsLemma": "What is the lemma?" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8048454348..6002813f68 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1772,7 +1772,7 @@ class ChatController extends State overlayEntry = MessageSelectionOverlay( chatController: this, event: event, - pangeaMessageEvent: pangeaMessageEvent, + timeline: timeline!, initialSelectedToken: selectedToken, nextEvent: nextEvent, prevEvent: prevEvent, diff --git a/lib/pangea/analytics_details_popup/morph_analytics_view.dart b/lib/pangea/analytics_details_popup/morph_analytics_view.dart index ef515d710e..41c668ddf0 100644 --- a/lib/pangea/analytics_details_popup/morph_analytics_view.dart +++ b/lib/pangea/analytics_details_popup/morph_analytics_view.dart @@ -1,17 +1,18 @@ +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; +import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_icon.dart'; -import 'package:fluffychat/pangea/morphs/morph_models.dart'; import 'package:fluffychat/pangea/user/client_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import '../morphs/morph_repo.dart'; class MorphAnalyticsView extends StatelessWidget { @@ -19,33 +20,49 @@ class MorphAnalyticsView extends StatelessWidget { super.key, }); - List get availableFeatures => MorphsRepo.get().displayFeatures; - @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(vertical: 20), - child: ListView.builder( - itemCount: availableFeatures.length, - itemBuilder: (context, index) => - availableFeatures[index].displayTags.isNotEmpty - ? MorphFeatureBox( - morphFeature: availableFeatures[index].feature, - ) - : const SizedBox.shrink(), + child: FutureBuilder( + future: MorphsRepo.get(), + builder: (context, snapshot) { + final morphs = snapshot.data ?? defaultMorphMapping; + + return snapshot.connectionState == ConnectionState.done + ? ListView.builder( + itemCount: morphs.displayFeatures.length, + itemBuilder: (context, index) => morphs + .displayFeatures[index].displayTags.isNotEmpty + ? MorphFeatureBox( + morphFeature: morphs.displayFeatures[index].feature, + allTags: snapshot.data + ?.getDisplayTags( + morphs.displayFeatures[index].feature, + ) + .map((tag) => tag.toLowerCase()) + .toSet() ?? + {}, + ) + : const SizedBox.shrink(), + ) + : const Center( + child: CircularProgressIndicator(), + ); + }, ), ); } class MorphFeatureBox extends StatelessWidget { final String morphFeature; + final Set allTags; const MorphFeatureBox({ super.key, required this.morphFeature, + required this.allTags, }); - // get constructData => MatrixState.pangeaController. - String _categoryCopy( String category, BuildContext context, @@ -61,11 +78,6 @@ class MorphFeatureBox extends StatelessWidget { category; } - Set get allTags => MorphsRepo.get() - .getDisplayTags(morphFeature) - .map((tag) => tag.toLowerCase()) - .toSet(); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -133,7 +145,9 @@ class MorphFeatureBox extends StatelessWidget { ), ), ) - .sortedBy((chip) => chip.constructAnalytics.points) + .sortedBy( + (chip) => chip.constructAnalytics.points, + ) .reversed .toList(), ), diff --git a/lib/pangea/analytics_misc/constructs_model.dart b/lib/pangea/analytics_misc/constructs_model.dart index 39963bff93..f744ff8e04 100644 --- a/lib/pangea/analytics_misc/constructs_model.dart +++ b/lib/pangea/analytics_misc/constructs_model.dart @@ -7,8 +7,8 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; import 'package:fluffychat/pangea/morphs/morph_models.dart'; -import '../morphs/morph_repo.dart'; import 'construct_type_enum.dart'; class ConstructAnalyticsModel { @@ -155,7 +155,7 @@ class OneConstructUse { return category ?? "Other"; } - final MorphFeatuuresAndTags morphs = MorphsRepo.get(); + final MorphFeaturesAndTags morphs = defaultMorphMapping; if (categoryEntry == null) { return morphs.guessMorphCategory(json["lemma"]); diff --git a/lib/pangea/common/constants/model_keys.dart b/lib/pangea/common/constants/model_keys.dart index 85f3d48948..357b8c189e 100644 --- a/lib/pangea/common/constants/model_keys.dart +++ b/lib/pangea/common/constants/model_keys.dart @@ -92,6 +92,7 @@ class ModelKey { /// something built in to matrix? should talk about this static const String messageTags = "p.tag"; static const String messageTagMorphEdit = "morph_edit"; + static const String messageTagLemmaEdit = "lemma_edit"; static const String messageTagActivityPlan = "activity_plan"; static const String baseDefinition = "base_definition"; diff --git a/lib/pangea/lemmas/lemma.dart b/lib/pangea/lemmas/lemma.dart index 1dc44c4b58..384a4f9723 100644 --- a/lib/pangea/lemmas/lemma.dart +++ b/lib/pangea/lemmas/lemma.dart @@ -1,7 +1,7 @@ /// Represents a lemma object class Lemma { /// [text] ex "ir" - text of the lemma of the word - final String text; + String text; /// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text final String form; diff --git a/lib/pangea/morphs/default_morph_mapping.dart b/lib/pangea/morphs/default_morph_mapping.dart index e3d89661ef..4a0118c335 100644 --- a/lib/pangea/morphs/default_morph_mapping.dart +++ b/lib/pangea/morphs/default_morph_mapping.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/pangea/morphs/morph_models.dart'; -final MorphFeatuuresAndTags defaultMorphMapping = - MorphFeatuuresAndTags.fromJson({ +final MorphFeaturesAndTags defaultMorphMapping = MorphFeaturesAndTags.fromJson({ "language_code": "default", "features": [ { diff --git a/lib/pangea/morphs/default_ud_mapping.dart b/lib/pangea/morphs/default_ud_mapping.dart index a389495b88..647ddcb7f2 100644 --- a/lib/pangea/morphs/default_ud_mapping.dart +++ b/lib/pangea/morphs/default_ud_mapping.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/pangea/morphs/morph_models.dart'; -final MorphFeatuuresAndTags defaultUDMapping = MorphFeatuuresAndTags.fromJson({ +final MorphFeaturesAndTags defaultUDMapping = MorphFeaturesAndTags.fromJson({ "language_code": "default", "features": [ { diff --git a/lib/pangea/morphs/morph_analytics_popup/morph_analytics_popup.dart b/lib/pangea/morphs/morph_analytics_popup/morph_analytics_popup.dart deleted file mode 100644 index 28a065bf5c..0000000000 --- a/lib/pangea/morphs/morph_analytics_popup/morph_analytics_popup.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_identifier.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; -import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; -import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; -import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; -import 'package:fluffychat/pangea/morphs/morph_icon.dart'; -import 'package:fluffychat/pangea/morphs/morph_models.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../../morphs/morph_repo.dart'; - -class MorphAnalyticsPopup extends StatelessWidget { - const MorphAnalyticsPopup({ - super.key, - }); - - List get availableFeatures => MorphsRepo.get().displayFeatures; - - @override - Widget build(BuildContext context) => FullWidthDialog( - dialogContent: Scaffold( - appBar: AppBar( - title: Text(ConstructTypeEnum.morph.indicator.tooltip(context)), - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: ListView.builder( - itemCount: availableFeatures.length, - itemBuilder: (context, index) => - availableFeatures[index].displayTags.isNotEmpty - ? MorphFeatureBox( - morphFeature: availableFeatures[index].feature, - ) - : const SizedBox.shrink(), - ), - ), - ), - maxWidth: 600, - maxHeight: 800, - ); -} - -class MorphFeatureBox extends StatelessWidget { - final String morphFeature; - - const MorphFeatureBox({ - super.key, - required this.morphFeature, - }); - - // get constructData => MatrixState.pangeaController. - - String _categoryCopy( - String category, - BuildContext context, - ) { - if (category.toLowerCase() == "other") { - return L10n.of(context).other; - } - - return ConstructTypeEnum.morph.getDisplayCopy( - category, - context, - ) ?? - category; - } - - Set get allTags => MorphsRepo.get() - .getDisplayTags(morphFeature) - .map((tag) => tag.toLowerCase()) - .toSet(); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - border: Border.all(color: AppConfig.gold.withAlpha(100), width: 2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 16.0, - children: [ - SizedBox( - height: 30.0, - width: 30.0, - child: MorphIcon(morphFeature: morphFeature, morphTag: null), - ), - Text( - _categoryCopy(morphFeature, context), - style: theme.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Wrap( - alignment: WrapAlignment.center, - spacing: 16.0, - runSpacing: 16.0, - children: allTags - .map( - (morphTag) => MorphTagChip( - morphFeature: morphFeature, - morphTag: morphTag, - constructAnalytics: MatrixState.pangeaController - .getAnalytics.constructListModel - .getConstructUses( - ConstructIdentifier( - lemma: morphTag, - type: ConstructTypeEnum.morph, - category: morphFeature, - ), - ) ?? - ConstructUses( - lemma: morphTag, - constructType: ConstructTypeEnum.morph, - category: morphFeature, - uses: [], - ), - ), - ) - .sortedBy((chip) => chip.constructAnalytics.points) - .reversed - .toList(), - ), - ), - ], - ), - ], - ), - ); - } -} - -class MorphTagChip extends StatelessWidget { - final String morphFeature; - final String morphTag; - final ConstructUses constructAnalytics; - - const MorphTagChip({ - super.key, - required this.morphFeature, - required this.morphTag, - required this.constructAnalytics, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Opacity( - opacity: constructAnalytics.points > 0 ? 1.0 : 0.3, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32.0), - gradient: constructAnalytics.points > 0 - ? LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.transparent, - constructAnalytics.lemmaCategory.color, - ], - ) - : null, - color: constructAnalytics.points > 0 ? null : theme.disabledColor, - ), - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - SizedBox( - width: 28.0, - height: 28.0, - child: constructAnalytics.points > 0 - ? MorphIcon( - morphFeature: morphFeature, - morphTag: morphTag, - ) - : const Icon( - Icons.lock, - color: Colors.white, - ), - ), - Text( - getGrammarCopy( - category: morphFeature, - lemma: morphTag, - context: context, - ) ?? - morphTag, - style: TextStyle( - fontWeight: FontWeight.bold, - color: theme.brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/morphs/morph_models.dart b/lib/pangea/morphs/morph_models.dart index 05d57e9196..faa057ac15 100644 --- a/lib/pangea/morphs/morph_models.dart +++ b/lib/pangea/morphs/morph_models.dart @@ -30,14 +30,14 @@ class MorphFeature { } } -class MorphFeatuuresAndTags { +class MorphFeaturesAndTags { final String languageCode; final List features; - MorphFeatuuresAndTags({required this.languageCode, required this.features}); + MorphFeaturesAndTags({required this.languageCode, required this.features}); - factory MorphFeatuuresAndTags.fromJson(Map json) { - return MorphFeatuuresAndTags( + factory MorphFeaturesAndTags.fromJson(Map json) { + return MorphFeaturesAndTags( languageCode: json['language_code'], features: List.from( json['features'].map((x) => MorphFeature.fromJson(x)), @@ -63,7 +63,9 @@ class MorphFeatuuresAndTags { /// i.e. minus punc, space, x, etc List getDisplayTags(String feature) => features - .firstWhereOrNull((element) => element.feature == feature) + .firstWhereOrNull( + (element) => element.feature.toLowerCase() == feature.toLowerCase(), + ) ?.displayTags ?? []; diff --git a/lib/pangea/morphs/morph_repo.dart b/lib/pangea/morphs/morph_repo.dart index c2ecbbe29a..cbfa812a01 100644 --- a/lib/pangea/morphs/morph_repo.dart +++ b/lib/pangea/morphs/morph_repo.dart @@ -16,7 +16,7 @@ import '../common/network/requests.dart'; class _APICallCacheItem { final DateTime time; - final Future future; + final Future future; _APICallCacheItem(this.time, this.future); } @@ -30,18 +30,18 @@ class MorphsRepo { static final shortTermCache = {}; static const int _cacheDurationMinutes = 1; - static void set(String languageCode, MorphFeatuuresAndTags response) { + static void set(String languageCode, MorphFeaturesAndTags response) { _morphsStorage.write( languageCode, response.toJson(), ); } - static MorphFeatuuresAndTags fromJson(Map json) { - return MorphFeatuuresAndTags.fromJson(json); + static MorphFeaturesAndTags fromJson(Map json) { + return MorphFeaturesAndTags.fromJson(json); } - static Future _fetch(String languageCode) async { + static Future _fetch(String languageCode) async { try { final Requests req = Requests( choreoApiKey: Environment.choreoApiKey, @@ -76,7 +76,7 @@ class MorphsRepo { /// if the morphs are not yet fetched. we'll see if this works well /// if not, we can make it async and update uses of this function /// to be async as well - static MorphFeatuuresAndTags get([String? languageCode]) { + static Future get([String? languageCode]) async { languageCode ??= MatrixState.pangeaController.languageController.userL2?.langCode; @@ -95,7 +95,7 @@ class MorphsRepo { if (cachedCall != null) { if (DateTime.now().difference(cachedCall.time).inMinutes < _cacheDurationMinutes) { - return defaultMorphMapping; + return cachedCall.future; } else { shortTermCache.remove(languageCode); } @@ -104,7 +104,6 @@ class MorphsRepo { // fetch the morphs but don't wait for it final future = _fetch(languageCode); shortTermCache[languageCode] = _APICallCacheItem(DateTime.now(), future); - - return defaultMorphMapping; + return future; } } diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 6fde8f464d..f25d093fa6 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -24,22 +24,22 @@ class MessageSelectionOverlay extends StatefulWidget { final Event _event; final Event? _nextEvent; final Event? _prevEvent; - final PangeaMessageEvent? _pangeaMessageEvent; final PangeaToken? _initialSelectedToken; + final Timeline _timeline; const MessageSelectionOverlay({ required this.chatController, required Event event, - required PangeaMessageEvent? pangeaMessageEvent, required PangeaToken? initialSelectedToken, required Event? nextEvent, required Event? prevEvent, + required Timeline timeline, super.key, }) : _initialSelectedToken = initialSelectedToken, - _pangeaMessageEvent = pangeaMessageEvent, _nextEvent = nextEvent, _prevEvent = prevEvent, - _event = event; + _event = event, + _timeline = timeline; @override MessageOverlayController createState() => MessageOverlayController(); @@ -54,7 +54,11 @@ class MessageOverlayController extends State List? tokens; bool initialized = false; - PangeaMessageEvent? get pangeaMessageEvent => widget._pangeaMessageEvent; + PangeaMessageEvent? get pangeaMessageEvent => PangeaMessageEvent( + event: widget._event, + timeline: widget._timeline, + ownMessage: widget._event.room.client.userID == widget._event.senderId, + ); bool isPlayingAudio = false; @@ -94,7 +98,7 @@ class MessageOverlayController extends State @override void initState() { super.initState(); - _initializeTokensAndMode(); + initializeTokensAndMode(); } void _updateSelectedSpan(PangeaTokenText selectedSpan) { @@ -104,7 +108,7 @@ class MessageOverlayController extends State widget.chatController.choreographer.tts.tryToSpeak( selectedSpan.content, context, - widget._pangeaMessageEvent?.eventId, + pangeaMessageEvent?.eventId, ); } @@ -144,7 +148,7 @@ class MessageOverlayController extends State ) : null; - Future _initializeTokensAndMode() async { + Future initializeTokensAndMode() async { try { final repEvent = pangeaMessageEvent?.messageDisplayRepresentation; if (repEvent != null) { @@ -174,7 +178,7 @@ class MessageOverlayController extends State MatrixState.pangeaController.languageController.userL2?.langCode; Future _setInitialToolbarMode() async { - if (widget._pangeaMessageEvent?.isAudioMessage ?? false) { + if (pangeaMessageEvent?.isAudioMessage ?? false) { toolbarMode = MessageMode.speechToText; return setState(() {}); } @@ -269,11 +273,10 @@ class MessageOverlayController extends State /// If there is a selectedSpan, then the target is the selected text String get targetText { if (_selectedSpan == null || pangeaMessageEvent == null) { - return widget._pangeaMessageEvent?.messageDisplayText ?? - widget._event.body; + return pangeaMessageEvent?.messageDisplayText ?? widget._event.body; } - return widget._pangeaMessageEvent!.messageDisplayText.substring( + return pangeaMessageEvent!.messageDisplayText.substring( _selectedSpan!.offset, _selectedSpan!.offset + _selectedSpan!.length, ); diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart index 492dbc57dc..cbe2e13af0 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart @@ -1,42 +1,194 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'package:fluffychat/pangea/analytics_misc/construct_level_enum.dart'; +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; -class LemmaWidget extends StatelessWidget { +class LemmaWidget extends StatefulWidget { final PangeaToken token; + final PangeaMessageEvent pangeaMessageEvent; + final VoidCallback onEdit; + final VoidCallback onEditDone; const LemmaWidget({ super.key, required this.token, + required this.pangeaMessageEvent, + required this.onEdit, + required this.onEditDone, }); + @override + LemmaWidgetState createState() => LemmaWidgetState(); +} + +class LemmaWidgetState extends State { + bool _editMode = false; + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _toggleEditMode(bool value) { + value ? widget.onEdit() : widget.onEditDone(); + setState(() => _editMode = value); + } + + Future _editLemma() async { + try { + final existingTokens = widget.pangeaMessageEvent.originalSent!.tokens! + .map((token) => PangeaToken.fromJson(token.toJson())) + .toList(); + + // change the morphological tag in the selected token + final tokenIndex = existingTokens.indexWhere( + (token) => token.text.offset == widget.token.text.offset, + ); + + if (tokenIndex == -1) { + throw Exception("Token not found in message"); + } + + existingTokens[tokenIndex].lemma.text = _controller.text; + await widget.pangeaMessageEvent.room.pangeaSendTextEvent( + widget.pangeaMessageEvent.messageDisplayText, + editEventId: widget.pangeaMessageEvent.eventId, + originalSent: widget.pangeaMessageEvent.originalSent?.content, + originalWritten: widget.pangeaMessageEvent.originalWritten?.content, + tokensSent: PangeaMessageTokens(tokens: existingTokens), + tokensWritten: widget.pangeaMessageEvent.originalWritten?.tokens != null + ? PangeaMessageTokens( + tokens: widget.pangeaMessageEvent.originalWritten!.tokens!, + ) + : null, + choreo: widget.pangeaMessageEvent.originalSent?.choreo, + messageTag: ModelKey.messageTagLemmaEdit, + ); + + _toggleEditMode(false); + } catch (e) { + SnackBar( + content: Text(L10n.of(context).oopsSomethingWentWrong), + ); + ErrorHandler.logError( + e: e, + data: { + "token": widget.token.toJson(), + "pangeaMessageEvent": widget.pangeaMessageEvent.event.content, + }, + ); + } + } + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 180), - child: Text( - token.lemma.text, - overflow: TextOverflow.ellipsis, + if (_editMode) { + _controller.text = widget.token.lemma.text; + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 10.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsLemma}", + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), ), - ), - const SizedBox(width: 6), - SizedBox( - width: 20, - height: 20, - child: CustomizedSvg( - svgUrl: token.lemmaXPCategory.svgURL, - colorReplacements: const {}, - errorIcon: Text(token.xpEmoji), + TextField( + minLines: 1, + maxLines: 3, + controller: _controller, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => _toggleEditMode(false), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + child: Text(L10n.of(context).cancel), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + _controller.text != widget.token.lemma.text + ? showFutureLoadingDialog( + context: context, + future: () async => _editLemma(), + ) + : null; + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + child: Text(L10n.of(context).saveChanges), + ), + ], + ), + ], + ), + ); + } + + return Flexible( + child: Tooltip( + triggerMode: TooltipTriggerMode.tap, + message: L10n.of(context).doubleClickToEdit, + child: GestureDetector( + onLongPress: () => _toggleEditMode(true), + onDoubleTap: () => _toggleEditMode(true), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 180), + child: Text( + widget.token.lemma.text, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 6), + SizedBox( + width: 20, + height: 20, + child: CustomizedSvg( + svgUrl: widget.token.lemmaXPCategory.svgURL, + colorReplacements: const {}, + errorIcon: Text(widget.token.xpEmoji), + ), + ), + ], ), ), - ], + ), ), ); } diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart index fcc909ec6e..6525fb37fa 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_categories_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_repo.dart'; @@ -23,11 +24,14 @@ class MorphologicalCenterWidget extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overlayController; + final VoidCallback onEditDone; + const MorphologicalCenterWidget({ required this.token, required this.morphFeature, required this.pangeaMessageEvent, required this.overlayController, + required this.onEditDone, super.key, }); @@ -118,9 +122,8 @@ class MorphologicalCenterWidgetState extends State { messageTag: ModelKey.messageTagMorphEdit, ); - setState(() { - editMode = false; - }); + setState(() => editMode = false); + widget.onEditDone(); } catch (e) { SnackBar( content: Text(L10n.of(context).oopsSomethingWentWrong), @@ -137,11 +140,6 @@ class MorphologicalCenterWidgetState extends State { } } - /// all morphological tags for the selected morphological category - /// that are eligible for setting as the morphological tag - List get allMorphTagsForEdit => - MorphsRepo.get().getDisplayTags(widget.morphFeature); - String get morphCopy => getMorphologicalCategoryCopy(widget.morphFeature, context) ?? widget.morphFeature; @@ -194,57 +192,75 @@ class MorphologicalCenterWidgetState extends State { scrollDirection: Axis.vertical, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Wrap( - alignment: WrapAlignment.center, - children: allMorphTagsForEdit.map((tag) { - return Container( - margin: const EdgeInsets.all(2), - padding: EdgeInsets.zero, - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(10)), - border: Border.all( - color: selectedMorphTag == tag - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - style: BorderStyle.solid, - width: 2.0, - ), - ), - child: TextButton( - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(horizontal: 7), - ), - backgroundColor: selectedMorphTag == tag - ? WidgetStateProperty.all( - Theme.of(context) - .colorScheme - .primary - .withAlpha(50), - ) - : null, - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - onPressed: () { - setState(() => selectedMorphTag = tag); - }, - child: Text( - getGrammarCopy( - category: widget.morphFeature, - lemma: tag, - context: context, - ) ?? - tag, - textAlign: TextAlign.center, - ), - ), - ); - }).toList(), + child: FutureBuilder( + future: MorphsRepo.get(), + builder: (context, snapshot) { + final allMorphTagsForEdit = + snapshot.data?.getDisplayTags(widget.morphFeature) ?? + defaultMorphMapping + .getDisplayTags(widget.morphFeature); + + return snapshot.connectionState == ConnectionState.done + ? Wrap( + alignment: WrapAlignment.center, + children: allMorphTagsForEdit.map((tag) { + return Container( + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + border: Border.all( + color: selectedMorphTag == tag + ? Theme.of(context) + .colorScheme + .primary + : Colors.transparent, + style: BorderStyle.solid, + width: 2.0, + ), + ), + child: TextButton( + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: 7, + ), + ), + backgroundColor: selectedMorphTag == tag + ? WidgetStateProperty.all( + Theme.of(context) + .colorScheme + .primary + .withAlpha(50), + ) + : null, + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + ), + onPressed: () { + setState(() => selectedMorphTag = tag); + }, + child: Text( + getGrammarCopy( + category: widget.morphFeature, + lemma: tag, + context: context, + ) ?? + tag, + textAlign: TextAlign.center, + ), + ), + ); + }).toList(), + ) + : const Center(child: CircularProgressIndicator()); + }, ), ), ), diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_center_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_center_widget.dart index 767c422e7d..1d3af12a2e 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_center_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_center_widget.dart @@ -45,6 +45,7 @@ class WordZoomCenterWidget extends StatelessWidget { morphFeature: selectedMorphFeature!, pangeaMessageEvent: wordDetailsController.widget.messageEvent, overlayController: overlayController, + onEditDone: wordDetailsController.onEditDone, ); case WordZoomSelection.lemma: return Text(token.lemma.text, textAlign: TextAlign.center); diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index b74f9d1fc3..1397326902 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -74,6 +74,8 @@ class WordZoomWidgetState extends State { // is computationally expensive, so we only do it once bool _canGenerateLemmaActivity = false; + bool _hideCenterContent = false; + @override void initState() { super.initState(); @@ -154,6 +156,10 @@ class WordZoomWidgetState extends State { if (mounted) setState(() {}); } + void _setHideCenterContent(bool value) { + if (mounted) setState(() => _hideCenterContent = value); + } + /// This function should be called before overlayController.onActivityFinish to /// prevent shouldDoActivity being set to false before _forceShowActivity is set to true. /// This keep the completed actvity visible to the user for a short time. @@ -184,6 +190,8 @@ class WordZoomWidgetState extends State { : true; } + void onEditDone() => widget.overlayController.initializeTokensAndMode(); + @override Widget build(BuildContext context) { return ConstrainedBox( @@ -220,37 +228,49 @@ class WordZoomWidgetState extends State { _setSelectionType(WordZoomSelection.emoji), isSelected: _selectionType == WordZoomSelection.emoji, ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - WordTextWithAudioButton( - text: widget.token.text.content, - ttsController: widget.tts, - eventID: widget.messageEvent.eventId, - ), - // if _selectionType is null, we don't know if the lemma activity - // can be shown yet, so we don't show the lemma definition - if (!_shouldShowActivity(WordZoomSelection.lemma) && - _selectionType != null) - LemmaWidget( - token: widget.token, + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + WordTextWithAudioButton( + text: widget.token.text.content, + ttsController: widget.tts, + eventID: widget.messageEvent.eventId, ), - ], + // if _selectionType is null, we don't know if the lemma activity + // can be shown yet, so we don't show the lemma definition + if (!_shouldShowActivity( + WordZoomSelection.lemma, + ) && + _selectionType != null) + LemmaWidget( + token: widget.token, + pangeaMessageEvent: widget.messageEvent, + onEdit: () => _setHideCenterContent(true), + onEditDone: () { + _setHideCenterContent(false); + onEditDone(); + }, + ), + ], + ), ), const SizedBox(width: 30), ], ), ), - WordZoomCenterWidget( - selectionType: _selectionType, - selectedMorphFeature: _selectedMorphFeature, - shouldDoActivity: _selectionType != null - ? _shouldShowActivity(_selectionType!) - : false, - locked: - _activityLock != null && !_activityLock!.isCompleted, - wordDetailsController: this, - ), + if (!_hideCenterContent) + WordZoomCenterWidget( + selectionType: _selectionType, + selectedMorphFeature: _selectedMorphFeature, + shouldDoActivity: _selectionType != null + ? _shouldShowActivity(_selectionType!) + : false, + locked: + _activityLock != null && !_activityLock!.isCompleted, + wordDetailsController: this, + ), MorphologicalListWidget( token: widget.token, setMorphFeature: (feature) => _setSelectionType( diff --git a/lib/pangea/user/client_extension.dart b/lib/pangea/user/client_extension.dart index 8977b4b1f8..bb4e8edb75 100644 --- a/lib/pangea/user/client_extension.dart +++ b/lib/pangea/user/client_extension.dart @@ -1,6 +1,7 @@ -import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; + extension AccountIdentiferExt on Client { bool get isSupportAccount => userID == Environment.supportUserId; } diff --git a/lib/pangea/user/controllers/user_controller.dart b/lib/pangea/user/controllers/user_controller.dart index be8e955679..2695737519 100644 --- a/lib/pangea/user/controllers/user_controller.dart +++ b/lib/pangea/user/controllers/user_controller.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:matrix/matrix.dart' as matrix; +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -32,7 +33,9 @@ class UserController extends BaseController { _profileListener ??= _pangeaController.matrixState.client.onSync.stream .where((sync) => sync.accountData != null) .listen((sync) { - final Profile? fromAccountData = Profile.fromAccountData(); + final profileData = _pangeaController + .matrixState.client.accountData[ModelKey.userProfile]?.content; + final Profile? fromAccountData = Profile.fromAccountData(profileData); if (fromAccountData != null) { _cachedProfile = fromAccountData; } @@ -52,7 +55,11 @@ class UserController extends BaseController { } /// try to get the account data in the up-to-date format - final Profile? fromAccountData = Profile.fromAccountData(); + final Profile? fromAccountData = Profile.fromAccountData( + _pangeaController + .matrixState.client.accountData[ModelKey.userProfile]?.content, + ); + if (fromAccountData != null) { _cachedProfile = fromAccountData; return fromAccountData; diff --git a/lib/pangea/user/models/user_model.dart b/lib/pangea/user/models/user_model.dart index 9aac617a4b..e971dddbd3 100644 --- a/lib/pangea/user/models/user_model.dart +++ b/lib/pangea/user/models/user_model.dart @@ -213,9 +213,7 @@ class Profile { } /// Load an instance of profile from the client's account data. - static Profile? fromAccountData() { - final profileData = MatrixState.pangeaController.matrixState.client - .accountData[ModelKey.userProfile]?.content; + static Profile? fromAccountData(Map? profileData) { if (profileData == null) return null; final userSettingsContent = profileData[ModelKey.userSettings];