From e96a16b297c8d9b69987ce8cac443fa17614cd9c Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 22 May 2025 13:00:42 -0400 Subject: [PATCH] =?UTF-8?q?chore:=20fully=20update=20match=20info=20after?= =?UTF-8?q?=20auto-accepting=20replacement,=20add=20=E2=80=A6=20(#2866)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: fully update match info after auto-accepting replacement, add more error handling in construct token span * bump version * fix: don't stop activity language on fail to fetch image URL * fix: don't show copy class code buttons into class code is null * fix: use activity type enum name in key instead of string --- lib/pages/chat/chat.dart | 11 +- .../invitation_selection_view.dart | 160 +++++++++--------- lib/pages/new_group/new_group.dart | 5 +- .../activity_planner/activity_plan_card.dart | 25 ++- .../widgets/class_invitation_buttons.dart | 5 +- .../controllers/choreographer.dart | 18 +- .../controllers/error_service.dart | 10 ++ .../controllers/igc_controller.dart | 3 + .../models/igc_text_data_model.dart | 65 +++++-- .../widgets/igc/pangea_text_controller.dart | 30 +++- .../room_space_settings_extension.dart | 6 +- .../activity_type_enum.dart | 19 --- .../message_activity_request.dart | 2 +- .../practice_activity_model.dart | 2 +- .../practice_selection_repo.dart | 2 +- .../practice_activities/practice_target.dart | 17 +- pubspec.yaml | 2 +- 17 files changed, 236 insertions(+), 146 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 754a9df95..815682d7c 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -867,10 +867,13 @@ class ChatController extends State pangeaEditingEvent = previousEdit; } - GoogleAnalytics.sendMessage( - room.id, - room.classCode(context), - ); + final spaceCode = room.classCode(context); + if (spaceCode != null) { + GoogleAnalytics.sendMessage( + room.id, + spaceCode, + ); + } if (msgEventId == null) { ErrorHandler.logError( diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index a27148870..0877cc645 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -63,98 +63,104 @@ class InvitationSelectionView extends StatelessWidget { child: Column( children: [ // #Pangea - Padding( - padding: const EdgeInsets.all(16.0), - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(99), - ), - onTap: () async { - await Clipboard.setData( - ClipboardData(text: room.classCode(context)), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).copiedToClipboard)), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 16.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, + if (room.isSpace && room.classCode(context) != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: InkWell( + customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(99), ), - child: Row( - spacing: 16.0, - children: [ - const Icon( - Icons.copy_outlined, - size: 20.0, + onTap: () async { + await Clipboard.setData( + ClipboardData(text: room.classCode(context)!), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context).copiedToClipboard), ), - Text( - "${L10n.of(context).copyClassCode}: ${room.classCode(context)}", - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, - fontSize: 16.0, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(99), + ), + child: Row( + spacing: 16.0, + children: [ + const Icon( + Icons.copy_outlined, + size: 20.0, ), - ), - ], + Text( + "${L10n.of(context).copyClassCode}: ${room.classCode(context)}", + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + fontSize: 16.0, + ), + ), + ], + ), ), ), ), - ), - Padding( - padding: const EdgeInsets.only( - bottom: 16.0, - left: 16.0, - right: 16.0, - ), - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(99), + if (room.isSpace && room.classCode(context) != null) + Padding( + padding: const EdgeInsets.only( + bottom: 16.0, + left: 16.0, + right: 16.0, ), - onTap: () async { - final String initialUrl = - kIsWeb ? html.window.origin! : Environment.frontendURL; - final link = - "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}"; - await Clipboard.setData(ClipboardData(text: link)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).copiedToClipboard)), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 16.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, + child: InkWell( + customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(99), ), - child: Row( - spacing: 16.0, - children: [ - const Icon( - Icons.copy_outlined, - size: 20.0, + onTap: () async { + final String initialUrl = + kIsWeb ? html.window.origin! : Environment.frontendURL; + final link = + "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}"; + await Clipboard.setData(ClipboardData(text: link)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context).copiedToClipboard), ), - Text( - L10n.of(context).copyClassLink, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, - fontSize: 16.0, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(99), + ), + child: Row( + spacing: 16.0, + children: [ + const Icon( + Icons.copy_outlined, + size: 20.0, ), - ), - ], + Text( + L10n.of(context).copyClassLink, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + fontSize: 16.0, + ), + ), + ], + ), ), ), ), - ), // Pangea# Padding( // #Pangea diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index b9d8467d0..b2b86036e 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -237,7 +237,10 @@ class NewGroupController extends State { room = client.getRoomById(spaceId); } if (room == null) return; - GoogleAnalytics.createClass(room.name, room.classCode(context)); + final spaceCode = room.classCode(context); + if (spaceCode != null) { + GoogleAnalytics.createClass(room.name, spaceCode); + } try { await room.invite(BotName.byEnvironment); } catch (err) { diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index 0d721e11e..c735f253a 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -174,12 +174,25 @@ class ActivityPlanCardState extends State { } Future _setAvatarByImageURL() async { - if (_avatar != null || _imageURL == null) return; - final resp = await http - .get(Uri.parse(_imageURL!)) - .timeout(const Duration(seconds: 5)); - if (mounted) { - setState(() => _avatar = resp.bodyBytes); + try { + if (_avatar != null || _imageURL == null) return; + final resp = await http + .get(Uri.parse(_imageURL!)) + .timeout(const Duration(seconds: 5)); + if (mounted) { + setState(() => _avatar = resp.bodyBytes); + } + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "imageURL": _imageURL, + }, + ); + if (mounted) { + setState(() => _avatar = null); + } } } diff --git a/lib/pangea/chat_settings/widgets/class_invitation_buttons.dart b/lib/pangea/chat_settings/widgets/class_invitation_buttons.dart index 8f2d6ad03..6e2dac6e5 100644 --- a/lib/pangea/chat_settings/widgets/class_invitation_buttons.dart +++ b/lib/pangea/chat_settings/widgets/class_invitation_buttons.dart @@ -19,6 +19,9 @@ class ClassInvitationButtons extends StatelessWidget { Widget build(BuildContext context) { final Room? room = Matrix.of(context).client.getRoomById(roomId); if (room == null) return Text(L10n.of(context).oopsSomethingWentWrong); + if (room.classCode(context) == null) { + return const SizedBox(); + } final copyClassLinkListTile = ListTile( title: Text( @@ -67,7 +70,7 @@ class ClassInvitationButtons extends StatelessWidget { onTap: () async { //PTODO-Lala: Standarize toast //PTODO - explore using Fluffyshare for this - await Clipboard.setData(ClipboardData(text: room.classCode(context))); + await Clipboard.setData(ClipboardData(text: room.classCode(context)!)); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n.of(context).copiedToClipboard)), ); diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index da59e978d..1733716d8 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -456,11 +456,6 @@ class Choreographer { if (!isNormalizationError) continue; final match = igc.igcTextData!.matches[i]; - choreoRecord.addRecord( - _textController.text, - match: match.copyWith..status = PangeaMatchStatus.automatic, - ); - igc.igcTextData!.acceptReplacement( i, match.match.choices!.indexWhere( @@ -468,6 +463,19 @@ class Choreographer { ), ); + final newMatch = match.copyWith; + newMatch.status = PangeaMatchStatus.automatic; + newMatch.match.length = match.match.choices! + .firstWhere((c) => c.isBestCorrection) + .value + .characters + .length; + + choreoRecord.addRecord( + _textController.text, + match: newMatch, + ); + _textController.setSystemText( igc.igcTextData!.originalInput, EditType.igc, diff --git a/lib/pangea/choreographer/controllers/error_service.dart b/lib/pangea/choreographer/controllers/error_service.dart index 94d1c83f3..2021815ff 100644 --- a/lib/pangea/choreographer/controllers/error_service.dart +++ b/lib/pangea/choreographer/controllers/error_service.dart @@ -65,7 +65,17 @@ class ErrorService { return Duration(seconds: coolDownSeconds); } + final List _errorCache = []; + setError(ChoreoError? error, {Duration? duration}) { + if (_errorCache.contains(error?.raw.toString())) { + return; + } + + if (error != null) { + _errorCache.add(error.raw.toString()); + } + _error = error; Future.delayed(duration ?? defaultCooldown, () { clear(); diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index cecf03ea0..03b034283 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -293,6 +293,9 @@ class IgcController { igcTextData = null; spanDataController.clearCache(); spanDataController.dispose(); + MatrixState.pAnyState.closeAllOverlays( + filter: RegExp(r'span_card_overlay_\d+'), + ); } dispose() { diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index ac6613b58..3991aa4f7 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -292,12 +292,45 @@ class IGCTextData { // create a pointer to the current index in the original input // and iterate until the pointer has reached the end of the input int currentIndex = 0; + int loops = 0; + final List addedMatches = []; while (currentIndex < originalInput.characters.length) { + if (loops > 100) { + ErrorHandler.logError( + e: "In constructTokenSpan, infinite loop detected", + data: { + "currentIndex": currentIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, infinite loop detected"; + } + // check if the pointer is at a match, and if so, get the index of the match final int matchIndex = matchRanges.indexWhere( (range) => currentIndex >= range[0] && currentIndex < range[1], ); - final bool inMatch = matchIndex != -1; + final bool inMatch = matchIndex != -1 && + !addedMatches.contains( + textSpanMatches[matchIndex], + ); + + if (matchIndex != -1 && + addedMatches.contains( + textSpanMatches[matchIndex], + )) { + ErrorHandler.logError( + e: "In constructTokenSpan, currentIndex is in match that has already been added", + data: { + "currentIndex": currentIndex, + "matchIndex": matchIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, currentIndex is in match that has already been added"; + } + + final prevIndex = currentIndex; if (inMatch) { // if the pointer is in a match, then add that match to items @@ -312,13 +345,7 @@ class IGCTextData { final span = originalInput.characters .getRange( match.match.offset, - match.match.offset + - (match.match.choices - ?.firstWhere((c) => c.isBestCorrection) - .value - .characters - .length ?? - match.match.length), + match.match.offset + match.match.length, ) .toString(); @@ -364,12 +391,8 @@ class IGCTextData { ), ); - currentIndex = match.match.offset + - (match.match.choices - ?.firstWhere((c) => c.isBestCorrection) - .value - .length ?? - match.match.length); + addedMatches.add(match); + currentIndex = match.match.offset + match.match.length; } else { items.add( getSpanItem( @@ -400,6 +423,20 @@ class IGCTextData { ); currentIndex = nextIndex; } + + if (prevIndex >= currentIndex) { + ErrorHandler.logError( + e: "In constructTokenSpan, currentIndex is less than prevIndex", + data: { + "currentIndex": currentIndex, + "prevIndex": prevIndex, + "matches": textSpanMatches.map((m) => m.toJson()).toList(), + }, + ); + throw "In constructTokenSpan, currentIndex is less than prevIndex"; + } + + loops++; } return items; diff --git a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart index d887eebbf..50ad90e68 100644 --- a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart'; @@ -174,18 +175,29 @@ class PangeaTextController extends TextEditingController { final choreoSteps = choreographer.choreoRecord.choreoSteps; + List inlineSpans = []; + try { + inlineSpans = choreographer.igc.igcTextData!.constructTokenSpan( + choreoSteps: choreoSteps.isNotEmpty && + choreoSteps.last.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.automatic + ? choreoSteps + : [], + defaultStyle: style, + onUndo: choreographer.onUndoReplacement, + ); + } catch (e) { + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: e), + ); + inlineSpans = [TextSpan(text: text, style: style)]; + choreographer.igc.clear(); + } + return TextSpan( style: style, children: [ - ...choreographer.igc.igcTextData!.constructTokenSpan( - choreoSteps: choreoSteps.isNotEmpty && - choreoSteps.last.acceptedOrIgnoredMatch?.status == - PangeaMatchStatus.automatic - ? choreoSteps - : [], - defaultStyle: style, - onUndo: choreographer.onUndoReplacement, - ), + ...inlineSpans, TextSpan(text: parts[1], style: style), ], ); diff --git a/lib/pangea/extensions/room_space_settings_extension.dart b/lib/pangea/extensions/room_space_settings_extension.dart index 3bd94f106..e5cdf1721 100644 --- a/lib/pangea/extensions/room_space_settings_extension.dart +++ b/lib/pangea/extensions/room_space_settings_extension.dart @@ -1,14 +1,14 @@ part of "pangea_room_extension.dart"; extension SpaceRoomExtension on Room { - String classCode(BuildContext context) { + String? classCode(BuildContext context) { if (!isSpace) { for (final Room potentialClassRoom in pangeaSpaceParents) { if (potentialClassRoom.isSpace) { return SpaceRoomExtension(potentialClassRoom).classCode(context); } } - return L10n.of(context).notInClass; + return null; } final roomJoinRules = getState(EventTypes.RoomJoinRules, ""); if (roomJoinRules != null) { @@ -17,7 +17,7 @@ extension SpaceRoomExtension on Room { return accessCode; } } - return L10n.of(context).noClassCode; + return null; } void checkClass() { diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart index 51e8ae106..2fe80c418 100644 --- a/lib/pangea/practice_activities/activity_type_enum.dart +++ b/lib/pangea/practice_activities/activity_type_enum.dart @@ -15,25 +15,6 @@ enum ActivityTypeEnum { } extension ActivityTypeExtension on ActivityTypeEnum { - String get string { - switch (this) { - case ActivityTypeEnum.wordMeaning: - return 'word_meaning'; - case ActivityTypeEnum.wordFocusListening: - return 'word_focus_listening'; - case ActivityTypeEnum.hiddenWordListening: - return 'hidden_word_listening'; - case ActivityTypeEnum.lemmaId: - return 'lemma_id'; - case ActivityTypeEnum.emoji: - return 'emoji'; - case ActivityTypeEnum.morphId: - return 'morph_id'; - case ActivityTypeEnum.messageMeaning: - return 'message_meaning'; // TODO: Add to L10n - } - } - bool get hiddenType { switch (this) { case ActivityTypeEnum.wordMeaning: diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 78907d196..f3427514e 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -83,7 +83,7 @@ class MessageActivityRequest { 'message_tokens': messageTokens.map((e) => e.toJson()).toList(), 'activity_quality_feedback': activityQualityFeedback?.toJson(), 'target_tokens': targetTokens.map((e) => e.toJson()).toList(), - 'target_type': targetType.string, + 'target_type': targetType.name, 'target_morph_feature': targetMorphFeature, }; } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 5dee155aa..68a0ce6b0 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -326,7 +326,7 @@ class PracticeActivityModel { Map toJson() { return { 'lang_code': langCode, - 'activity_type': activityType.string, + 'activity_type': activityType.name, 'content': multipleChoiceContent?.toJson(), 'target_tokens': targetTokens.map((e) => e.toJson()).toList(), 'match_content': matchContent?.toJson(), diff --git a/lib/pangea/practice_activities/practice_selection_repo.dart b/lib/pangea/practice_activities/practice_selection_repo.dart index 3e31eb0f6..fc97ed381 100644 --- a/lib/pangea/practice_activities/practice_selection_repo.dart +++ b/lib/pangea/practice_activities/practice_selection_repo.dart @@ -48,7 +48,7 @@ class PracticeSelectionRepo { } static void clean() { - final Iterable keys = _storage.getKeys(); + final keys = _storage.getKeys(); if (keys.length > 300) { final entries = keys .map((key) => _parsePracticeSelection(key)) diff --git a/lib/pangea/practice_activities/practice_target.dart b/lib/pangea/practice_activities/practice_target.dart index df034d20b..fbb711f2a 100644 --- a/lib/pangea/practice_activities/practice_target.dart +++ b/lib/pangea/practice_activities/practice_target.dart @@ -2,6 +2,8 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; +import 'package:collection/collection.dart'; + import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; @@ -60,13 +62,22 @@ class PracticeTarget { userL2.hashCode; static PracticeTarget fromJson(Map json) { + final type = ActivityTypeEnum.values.firstWhereOrNull( + (v) => json['activityType'] == v.name, + ); + if (type == null) { + throw Exception( + "ActivityTypeEnum ${json['activityType']} not found in enum", + ); + } + return PracticeTarget( tokens: (json['tokens'] as List).map((e) => PangeaToken.fromJson(e)).toList(), - activityType: ActivityTypeEnum.values[json['activityType']], + activityType: type, morphFeature: json['morphFeature'] == null ? null - : MorphFeaturesEnum.values[json['morphFeature']], + : MorphFeaturesEnumExtension.fromString(json['morphFeature']), userL2: json['userL2'], ); } @@ -83,7 +94,7 @@ class PracticeTarget { //unique condensed deterministic key for local storage String get storageKey { return tokens.map((e) => e.text.content).join() + - activityType.string + + activityType.name + (morphFeature?.name ?? ""); } diff --git a/pubspec.yaml b/pubspec.yaml index 894a183f4..9b9574d26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 4.1.10+1 +version: 4.1.10+2 environment: sdk: ">=3.0.0 <4.0.0"