From a34103793f7206ad8a7b49d14b94f581c5c0de61 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:10:25 -0500 Subject: [PATCH 1/6] fix: don't reset language last fetched unless made call to server, add bot settings to main popup menu --- lib/pages/chat/events/message.dart | 2 +- .../activity_participant_indicator.dart | 2 +- .../analytics_data_service.dart | 4 +- .../analytics_sync_controller.dart | 21 +- .../analytics_update_service.dart | 16 +- .../bot/widgets/bot_chat_settings_dialog.dart | 249 +++++++++--------- .../widgets/bot_settings_language_icon.dart | 10 +- lib/pangea/languages/p_language_store.dart | 20 +- .../member_actions_popup_menu_button.dart | 28 +- pubspec.yaml | 2 +- 10 files changed, 182 insertions(+), 172 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 15273e9fb..d3ac2306b 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -482,7 +482,7 @@ class Message extends StatelessWidget { miniIcon: user.id == BotName.byEnvironment ? BotSettingsLanguageIcon( - room: controller.room, + user: user, ) : null, presenceOffset: diff --git a/lib/pangea/activity_sessions/activity_participant_indicator.dart b/lib/pangea/activity_sessions/activity_participant_indicator.dart index 0f85ff327..58fd0403f 100644 --- a/lib/pangea/activity_sessions/activity_participant_indicator.dart +++ b/lib/pangea/activity_sessions/activity_participant_indicator.dart @@ -72,7 +72,7 @@ class ActivityParticipantIndicator extends StatelessWidget { userId: userId, miniIcon: room != null && userId == BotName.byEnvironment - ? BotSettingsLanguageIcon(room: room!) + ? BotSettingsLanguageIcon(user: user!) : null, presenceOffset: room != null && userId == BotName.byEnvironment diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index f3084c061..f1312c625 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -208,8 +208,8 @@ class AnalyticsDataService { return analyticsRoom?.blockedConstructs ?? {}; } - Future waitForSync() async { - await _syncController?.syncStream.stream.first; + Future waitForSync(String analyticsRoomID) async { + await _syncController?.waitForSync(analyticsRoomID); } Future get derivedData async { diff --git a/lib/pangea/analytics_data/analytics_sync_controller.dart b/lib/pangea/analytics_data/analytics_sync_controller.dart index d0aaaccee..53ccd760a 100644 --- a/lib/pangea/analytics_data/analytics_sync_controller.dart +++ b/lib/pangea/analytics_data/analytics_sync_controller.dart @@ -14,8 +14,6 @@ class AnalyticsSyncController { final AnalyticsDataService dataService; StreamSubscription? _subscription; - StreamController> syncStream = - StreamController>.broadcast(); AnalyticsSyncController({ required this.client, @@ -29,7 +27,6 @@ class AnalyticsSyncController { void dispose() { _subscription?.cancel(); _subscription = null; - syncStream.close(); } Future _onSync(SyncUpdate update) async { @@ -55,10 +52,22 @@ class AnalyticsSyncController { if (constructEvents.isEmpty) return; await dataService.updateServerAnalytics(constructEvents); + } - syncStream.add( - List.from(constructEvents.map((e) => e.event.eventId)), - ); + Future waitForSync(String analyticsRoomId) async { + await client.onSync.stream.firstWhere((update) { + final roomUpdate = update.rooms?.join?[analyticsRoomId]; + if (roomUpdate == null) return false; + + final hasAnalyticsEvent = roomUpdate.timeline?.events?.any( + (e) => + e.type == PangeaEventTypes.construct && + e.senderId == client.userID, + ) ?? + false; + + return hasAnalyticsEvent; + }); } Future bulkUpdate() async { diff --git a/lib/pangea/analytics_data/analytics_update_service.dart b/lib/pangea/analytics_data/analytics_update_service.dart index 29c0cbaf9..cdb832650 100644 --- a/lib/pangea/analytics_data/analytics_update_service.dart +++ b/lib/pangea/analytics_data/analytics_update_service.dart @@ -29,8 +29,8 @@ class AnalyticsUpdateService { LanguageModel? get _l2 => MatrixState.pangeaController.userController.userL2; - Future _getAnalyticsRoom() async { - final l2 = _l2; + Future _getAnalyticsRoom({LanguageModel? l2Override}) async { + final l2 = l2Override ?? _l2; if (l2 == null) return null; final analyticsRoom = await dataService.getAnalyticsRoom(l2); @@ -101,11 +101,17 @@ class AnalyticsUpdateService { Future _updateAnalytics({LanguageModel? l2Override}) async { final localConstructs = await dataService.getLocalUses(); if (localConstructs.isEmpty) return; - final analyticsRoom = await _getAnalyticsRoom(); + final analyticsRoom = await _getAnalyticsRoom(l2Override: l2Override); + if (analyticsRoom == null) { + debugPrint( + "No analytics room found for L2 Override: ${l2Override?.langCode}", + ); + return; + } // and send cached analytics data to the room - final future = dataService.waitForSync(); - await analyticsRoom?.sendConstructsEvent(localConstructs); + final future = dataService.waitForSync(analyticsRoom.id); + await analyticsRoom.sendConstructsEvent(localConstructs); await future; } diff --git a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart index 663b1f217..87c603de2 100644 --- a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart +++ b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart @@ -9,34 +9,17 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart'; import 'package:fluffychat/pangea/languages/language_model.dart'; import 'package:fluffychat/pangea/languages/p_language_store.dart'; import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart'; import 'package:fluffychat/pangea/learning_settings/p_language_dropdown.dart'; -import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class BotChatSettingsDialog extends StatefulWidget { final Room room; - static Future show({ - required BuildContext context, - required Room room, - }) async { - final resp = await showAdaptiveDialog( - context: context, - barrierDismissible: true, - builder: (context) => BotChatSettingsDialog(room: room), - ); - if (resp == null) return; - await showFutureLoadingDialog( - context: context, - future: () => room.setBotOptions(resp), - ); - } - const BotChatSettingsDialog({ required this.room, super.key, @@ -68,125 +51,143 @@ class BotChatSettingsDialogState extends State { bool get _isActivity => widget.room.isActivitySession; - void _setLanguage(LanguageModel? lang) => - setState(() => _selectedLang = lang); - void _setLevel(LanguageLevelTypeEnum? level) => - setState(() => _selectedLevel = level); - void _setVoice(String? voice) => setState(() => _selectedVoice = voice); + Future _setLanguage(LanguageModel? lang) async { + setState(() { + _selectedLang = lang; + _selectedVoice = null; + }); - BotOptionsModel get _updatedModel { - final botSettings = widget.room.botOptions ?? BotOptionsModel(); - if (_selectedLang != null) { - botSettings.targetLanguage = _selectedLang!.langCode; + final model = widget.room.botOptions ?? BotOptionsModel(); + model.targetLanguage = lang?.langCode; + model.targetVoice = null; + + try { + await widget.room.setBotOptions(model); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': widget.room.id, + 'langCode': lang?.langCode, + }, + ); } - if (_selectedLevel != null) { - botSettings.languageLevel = _selectedLevel!; + } + + Future _setLevel(LanguageLevelTypeEnum? level) async { + if (level == null) return; + + setState(() => _selectedLevel = level); + final model = widget.room.botOptions ?? BotOptionsModel(); + model.languageLevel = level; + try { + await widget.room.setBotOptions(model); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': widget.room.id, + 'level': level.name, + }, + ); } - if (_selectedVoice != null) { - botSettings.targetVoice = _selectedVoice; + } + + Future _setVoice(String? voice) async { + setState(() => _selectedVoice = voice); + final model = widget.room.botOptions ?? BotOptionsModel(); + model.targetVoice = voice; + try { + await widget.room.setBotOptions(model); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': widget.room.id, + 'voice': voice, + }, + ); } - return botSettings; } @override Widget build(BuildContext context) { - return AlertDialog.adaptive( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256), - child: Center( - child: Text( - L10n.of(context).botSettings, - textAlign: TextAlign.center, + return Material( + type: MaterialType.transparency, + child: Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.room.isActivitySession) + ListTile( + contentPadding: const EdgeInsets.all(0.0), + minLeadingWidth: 12.0, + leading: Icon( + Icons.info_outline, + size: 12.0, + color: Theme.of(context).disabledColor, + ), + title: Text( + L10n.of(context).activitySettingsOverrideWarning, + style: TextStyle( + color: Theme.of(context).disabledColor, + fontSize: 12.0, + ), + ), + ) + else + const SizedBox(), + PLanguageDropdown( + onChange: _setLanguage, + initialLanguage: _selectedLang, + languages: + MatrixState.pangeaController.pLanguageStore.targetOptions, + isL2List: true, + decorationText: L10n.of(context).targetLanguage, + enabled: !widget.room.isActivitySession, ), - ), - ), - content: Material( - type: MaterialType.transparency, - child: Container( - padding: const EdgeInsets.only(top: 16.0), - constraints: const BoxConstraints( - maxWidth: 256, - maxHeight: 300, + LanguageLevelDropdown( + initialLevel: _selectedLevel, + onChanged: _setLevel, + enabled: !widget.room.isActivitySession, ), - child: SingleChildScrollView( - child: Column( - spacing: 16.0, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (_isActivity) - ListTile( - contentPadding: const EdgeInsets.all(0.0), - minLeadingWidth: 12.0, - leading: Icon( - Icons.info_outline, - size: 12.0, - color: Theme.of(context).disabledColor, - ), - title: Text( - L10n.of(context).activitySettingsOverrideWarning, - style: TextStyle( - color: Theme.of(context).disabledColor, - fontSize: 12.0, - ), - ), - ), - PLanguageDropdown( - onChange: _setLanguage, - initialLanguage: _selectedLang, - languages: - MatrixState.pangeaController.pLanguageStore.targetOptions, - isL2List: true, - decorationText: L10n.of(context).targetLanguage, - enabled: !_isActivity, - ), - LanguageLevelDropdown( - initialLevel: _selectedLevel, - onChanged: _setLevel, - enabled: !_isActivity, - ), - DropdownButtonFormField2( - customButton: _selectedVoice != null - ? CustomDropdownTextButton(text: _selectedVoice!) - : null, - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - ), - decoration: InputDecoration( - labelText: L10n.of(context).voice, - ), - isExpanded: true, - dropdownStyleData: DropdownStyleData( - maxHeight: kIsWeb ? 250 : null, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14.0), - ), - ), - items: (_selectedLang?.voices ?? []).map((voice) { - return DropdownMenuItem( - value: voice, - child: Text(voice), - ); - }).toList(), - onChanged: _setVoice, - value: _selectedVoice, - ), - ], + DropdownButtonFormField2( + customButton: _selectedVoice != null + ? CustomDropdownTextButton(text: _selectedVoice!) + : null, + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), ), + decoration: InputDecoration( + labelText: L10n.of(context).voice, + ), + isExpanded: true, + dropdownStyleData: DropdownStyleData( + maxHeight: kIsWeb ? 250 : null, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(14.0), + ), + ), + items: (_selectedLang?.voices ?? []).map((voice) { + return DropdownMenuItem( + value: voice, + child: Text(voice), + ); + }).toList(), + onChanged: _setVoice, + value: _selectedVoice, ), - ), + const SizedBox(), + ], ), - actions: [ - AdaptiveDialogAction( - bigButtons: true, - onPressed: () => Navigator.of(context).pop(_updatedModel), - child: Text(L10n.of(context).submit), - ), - ], ); } } diff --git a/lib/pangea/bot/widgets/bot_settings_language_icon.dart b/lib/pangea/bot/widgets/bot_settings_language_icon.dart index 7a922454b..837bba59f 100644 --- a/lib/pangea/bot/widgets/bot_settings_language_icon.dart +++ b/lib/pangea/bot/widgets/bot_settings_language_icon.dart @@ -4,19 +4,20 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart'; -import 'package:fluffychat/pangea/bot/widgets/bot_chat_settings_dialog.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart'; class BotSettingsLanguageIcon extends StatelessWidget { - final Room room; + final User user; const BotSettingsLanguageIcon({ super.key, - required this.room, + required this.user, }); @override Widget build(BuildContext context) { + final room = user.room; String? langCode = room.botOptions?.targetLanguage; if (room.isActivitySession && room.activityPlan != null) { langCode = room.activityPlan!.req.targetLanguage; @@ -29,8 +30,9 @@ class BotSettingsLanguageIcon extends StatelessWidget { return InkWell( borderRadius: BorderRadius.circular(32.0), onTap: room.isRoomAdmin - ? () => BotChatSettingsDialog.show( + ? () => showMemberActionsPopupMenu( context: context, + user: user, room: room, ) : null, diff --git a/lib/pangea/languages/p_language_store.dart b/lib/pangea/languages/p_language_store.dart index 02c238b88..60327881b 100644 --- a/lib/pangea/languages/p_language_store.dart +++ b/lib/pangea/languages/p_language_store.dart @@ -49,17 +49,17 @@ class PLanguageStore { : LanguageConstants.languageList .map((e) => LanguageModel.fromJson(e)) .toList(); + + await _MyShared.saveJson(PrefKey.languagesKey, { + PrefKey.languagesKey: _langList.map((e) => e.toJson()).toList(), + }); + + await _MyShared.saveString( + PrefKey.lastFetched, + DateTime.now().toIso8601String(), + ); } - await _MyShared.saveJson(PrefKey.languagesKey, { - PrefKey.languagesKey: _langList.map((e) => e.toJson()).toList(), - }); - - await _MyShared.saveString( - PrefKey.lastFetched, - DateTime.now().toIso8601String(), - ); - _langList.removeWhere( (element) => element.langCode == LanguageKeys.unknownLanguage, ); @@ -78,7 +78,7 @@ class PLanguageStore { return true; } - final DateTime targetDate = DateTime(2026, 1, 9); + final DateTime targetDate = DateTime(2026, 1, 15); if (lastFetchedDate.isBefore(targetDate)) { return true; } diff --git a/lib/widgets/member_actions_popup_menu_button.dart b/lib/widgets/member_actions_popup_menu_button.dart index 5c444dead..75376e351 100644 --- a/lib/widgets/member_actions_popup_menu_button.dart +++ b/lib/widgets/member_actions_popup_menu_button.dart @@ -59,6 +59,7 @@ void showMemberActionsPopupMenu({ PopupMenuItem( value: _MemberActions.info, child: Row( + // Pangea# spacing: 12.0, children: [ Avatar( @@ -107,6 +108,15 @@ void showMemberActionsPopupMenu({ ], ), ), + if (user.id == BotName.byEnvironment && room != null && room.isRoomAdmin) + PopupMenuItem( + enabled: false, + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + ), + child: BotChatSettingsDialog(room: room), + ), const PopupMenuDivider(), // #Pangea if (user.room.client.userID != user.id) @@ -124,17 +134,6 @@ void showMemberActionsPopupMenu({ ], ), ), - if (user.id == BotName.byEnvironment && room != null && room.isRoomAdmin) - PopupMenuItem( - value: _MemberActions.botSettings, - child: Row( - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 18), - Text(L10n.of(context).botSettings), - ], - ), - ), // Pangea# if (onMention != null) PopupMenuItem( @@ -356,12 +355,6 @@ void showMemberActionsPopupMenu({ final roomId = roomIdResult.result; if (roomId == null) return; router.go('/rooms/$roomId'); - case _MemberActions.botSettings: - await BotChatSettingsDialog.show( - context: context, - room: room!, - ); - return; // Pangea# case _MemberActions.info: await UserDialog.show( @@ -401,6 +394,5 @@ enum _MemberActions { // #Pangea // report, chat, - botSettings, // Pangea# } diff --git a/pubspec.yaml b/pubspec.yaml index 77f4dbc51..0e30c0b85 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.16+1 +version: 4.1.16+2 environment: sdk: ">=3.0.0 <4.0.0" From d36ae6515486c363ec9ea5e07d5f91841ef3f965 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 09:42:08 -0500 Subject: [PATCH 2/6] chore: analytics details page tweaks --- .../construct_xp_progress_bar.dart | 36 ++++------- .../vocab_analytics_details_view.dart | 59 ++++++++++--------- .../analytics_misc/analytics_constants.dart | 2 +- .../toolbar/word_card/word_zoom_widget.dart | 6 +- 4 files changed, 47 insertions(+), 56 deletions(-) diff --git a/lib/pangea/analytics_details_popup/construct_xp_progress_bar.dart b/lib/pangea/analytics_details_popup/construct_xp_progress_bar.dart index 4089a46b1..794cdbe5f 100644 --- a/lib/pangea/analytics_details_popup/construct_xp_progress_bar.dart +++ b/lib/pangea/analytics_details_popup/construct_xp_progress_bar.dart @@ -41,32 +41,16 @@ class ConstructXPProgressBar extends StatelessWidget { return Column( spacing: 8.0, children: [ - LayoutBuilder( - builder: (context, constraints) { - double availableGap = - constraints.maxWidth - (categories.length * iconSize); - const totalPoints = AnalyticsConstants.xpForFlower; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ...categories.map( - (c) { - final gapPercent = (c.xpNeeded / totalPoints); - final gap = availableGap * gapPercent; - availableGap -= gap; - return Container( - width: iconSize + gap, - alignment: Alignment.centerRight, - child: Opacity( - opacity: level == c ? 1.0 : 0.4, - child: c.icon(iconSize), - ), - ); - }, - ), - ], - ); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...categories.map( + (c) => Opacity( + opacity: level == c ? 1.0 : 0.4, + child: c.icon(iconSize), + ), + ), + ], ), AnimatedProgressBar( height: 20.0, diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 6de39e654..58bafc387 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -82,35 +82,40 @@ class VocabDetailsView extends StatelessWidget { maxWidth: 600.0, showBorder: false, child: Column( - spacing: 16.0, + spacing: 20.0, children: [ - WordZoomWidget( - token: tokenText, - langCode: - MatrixState.pangeaController.userController.userL2Code!, - construct: constructId, - onClose: Navigator.of(context).pop, - onFlagTokenInfo: - (LemmaInfoResponse lemmaInfo, String phonetics) { - final requestData = TokenInfoFeedbackRequestData( - userId: Matrix.of(context).client.userID!, - detectedLanguage: - MatrixState.pangeaController.userController.userL2Code!, - tokens: [token], - selectedToken: 0, - wordCardL1: - MatrixState.pangeaController.userController.userL1Code!, - lemmaInfo: lemmaInfo, - phonetics: phonetics, - ); + const SizedBox(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: WordZoomWidget( + token: tokenText, + langCode: + MatrixState.pangeaController.userController.userL2Code!, + construct: constructId, + onClose: Navigator.of(context).pop, + onFlagTokenInfo: + (LemmaInfoResponse lemmaInfo, String phonetics) { + final requestData = TokenInfoFeedbackRequestData( + userId: Matrix.of(context).client.userID!, + detectedLanguage: MatrixState + .pangeaController.userController.userL2Code!, + tokens: [token], + selectedToken: 0, + wordCardL1: MatrixState + .pangeaController.userController.userL1Code!, + lemmaInfo: lemmaInfo, + phonetics: phonetics, + ); - TokenFeedbackUtil.showTokenFeedbackDialog( - context, - requestData: requestData, - langCode: - MatrixState.pangeaController.userController.userL2Code!, - ); - }, + TokenFeedbackUtil.showTokenFeedbackDialog( + context, + requestData: requestData, + langCode: MatrixState + .pangeaController.userController.userL2Code!, + ); + }, + maxWidth: double.infinity, + ), ), if (construct != null) Column( diff --git a/lib/pangea/analytics_misc/analytics_constants.dart b/lib/pangea/analytics_misc/analytics_constants.dart index d680c1fd7..96fb33a13 100644 --- a/lib/pangea/analytics_misc/analytics_constants.dart +++ b/lib/pangea/analytics_misc/analytics_constants.dart @@ -2,7 +2,7 @@ class AnalyticsConstants { static const int xpPerLevel = 500; static const int vocabUseMaxXP = 30; static const int morphUseMaxXP = 500; - static const int xpForGreens = 30; + static const int xpForGreens = 50; static const int xpForFlower = 100; static const String seedSvgFileName = "Seed.svg"; static const String leafSvgFileName = "Leaf.svg"; diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart index f8094f26c..0d53ba459 100644 --- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart +++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart @@ -30,6 +30,7 @@ class WordZoomWidget extends StatelessWidget { final bool enableEmojiSelection; final VoidCallback? onDismissNewWordOverlay; final Function(LemmaInfoResponse, String)? onFlagTokenInfo; + final double? maxWidth; const WordZoomWidget({ super.key, @@ -41,6 +42,7 @@ class WordZoomWidget extends StatelessWidget { this.enableEmojiSelection = true, this.onDismissNewWordOverlay, this.onFlagTokenInfo, + this.maxWidth, }); String get transformTargetId => "word-zoom-card-${token.uniqueKey}"; @@ -63,8 +65,8 @@ class WordZoomWidget extends StatelessWidget { Container( height: AppConfig.toolbarMaxHeight - 8, padding: const EdgeInsets.all(12.0), - constraints: const BoxConstraints( - maxWidth: AppConfig.toolbarMinWidth, + constraints: BoxConstraints( + maxWidth: maxWidth ?? AppConfig.toolbarMinWidth, ), child: CompositedTransformTarget( link: layerLink, From 633c82a6d03ab71997e0a9f416ab73ab4f5f19ac Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 10:00:26 -0500 Subject: [PATCH 3/6] fix: show usage dots for 0 xp usage in grey --- .../lemma_usage_dots.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/pangea/analytics_details_popup/lemma_usage_dots.dart b/lib/pangea/analytics_details_popup/lemma_usage_dots.dart index aa4e78e83..6c4f90087 100644 --- a/lib/pangea/analytics_details_popup/lemma_usage_dots.dart +++ b/lib/pangea/analytics_details_popup/lemma_usage_dots.dart @@ -24,16 +24,19 @@ class LemmaUsageDots extends StatelessWidget { }); /// Find lemma uses for the given exercise type, to create dot list - List sortedUses(LearningSkillsEnum category) { - final List useList = []; + List sortedUses(LearningSkillsEnum category) { + final List useList = []; for (final OneConstructUse use in construct.cappedUses) { - if (use.xp == 0) { - continue; - } // If the use type matches the given category, save to list // Usage with positive XP is saved as true, else false if (category == use.useType.skillsEnumType) { - useList.add(use.xp > 0); + useList.add( + switch (use.xp) { + > 0 => AppConfig.success, + < 0 => Colors.red, + _ => Colors.grey[400]!, + }, + ); } } return useList; @@ -42,13 +45,13 @@ class LemmaUsageDots extends StatelessWidget { @override Widget build(BuildContext context) { final List dots = []; - for (final bool use in sortedUses(category)) { + for (final Color color in sortedUses(category)) { dots.add( Container( width: 15.0, height: 15.0, decoration: BoxDecoration( - color: use ? AppConfig.success : Colors.red, + color: color, shape: BoxShape.circle, ), ), From 3d43469291db20c7105237e9265555c11bf453d0 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 16 Jan 2026 10:01:10 -0500 Subject: [PATCH 4/6] Add weight icon to practice vocab button --- .../analytics_details_popup.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pangea/analytics_details_popup/analytics_details_popup.dart b/lib/pangea/analytics_details_popup/analytics_details_popup.dart index 6661afa0a..3a248ee1d 100644 --- a/lib/pangea/analytics_details_popup/analytics_details_popup.dart +++ b/lib/pangea/analytics_details_popup/analytics_details_popup.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:diacritic/diacritic.dart'; import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart'; @@ -248,10 +249,11 @@ Widget _buildVocabPracticeButton(BuildContext context) { label: Row( mainAxisSize: MainAxisSize.min, children: [ - if (!hasEnoughVocab) ...[ - const Icon(Icons.lock_outline, size: 18), - const SizedBox(width: 4), - ], + Icon( + hasEnoughVocab ? Symbols.fitness_center : Icons.lock_outline, + size: 18, + ), + const SizedBox(width: 4), Text(L10n.of(context).practiceVocab), ], ), From e37a1857f393a1eb2366a1a02ec40dce68bf5ca3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 10:04:52 -0500 Subject: [PATCH 5/6] fix: highlight level bar button when viewing level analytics --- .../analytics_summary/learning_progress_indicators.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pangea/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index 6a70b9758..d89851c6c 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -175,7 +175,9 @@ class LearningProgressIndicators extends StatelessWidget { builder: (context, hovered) { return Container( decoration: BoxDecoration( - color: hovered && canSelect + color: (hovered && canSelect) || + (selected == + ProgressIndicatorEnum.level) ? Theme.of(context) .colorScheme .primary From db31adb051472dd04695fd2ae55fe6f3c67d635e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 10:13:39 -0500 Subject: [PATCH 6/6] chore: log sentry error when token POS is other --- lib/pangea/events/repo/tokens_repo.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pangea/events/repo/tokens_repo.dart b/lib/pangea/events/repo/tokens_repo.dart index 0f9d8b4d6..0b849bc3c 100644 --- a/lib/pangea/events/repo/tokens_repo.dart +++ b/lib/pangea/events/repo/tokens_repo.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:async/async.dart'; import 'package:http/http.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; @@ -62,7 +63,18 @@ class TokensRepo { final Map json = jsonDecode(utf8.decode(res.bodyBytes).toString()); - return TokensResponseModel.fromJson(json); + final tokens = TokensResponseModel.fromJson(json); + if (tokens.tokens.any((t) => t.pos == 'other')) { + ErrorHandler.logError( + e: Exception('Received token with pos "other"'), + data: { + "request": request.toJson(), + "response": json, + }, + level: SentryLevel.warning, + ); + } + return tokens; } static Future> _getResult(