From 633c82a6d03ab71997e0a9f416ab73ab4f5f19ac Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 10:00:26 -0500 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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( From a17aede84e69604f574220c9850fefb08d027270 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 10:32:17 -0500 Subject: [PATCH 5/7] fix: always add padding around practice page content --- .../vocab_practice/vocab_practice_view.dart | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/pangea/vocab_practice/vocab_practice_view.dart b/lib/pangea/vocab_practice/vocab_practice_view.dart index a196e4186..7fd3783e8 100644 --- a/lib/pangea/vocab_practice/vocab_practice_view.dart +++ b/lib/pangea/vocab_practice/vocab_practice_view.dart @@ -67,26 +67,28 @@ class VocabPracticeView extends StatelessWidget { ], ), ), - body: MaxWidthBody( - withScrolling: false, + body: Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 24.0, ), - showBorder: false, - child: ValueListenableBuilder( - valueListenable: controller.sessionState, - builder: (context, state, __) { - return switch (state) { - AsyncError(:final error) => - ErrorIndicator(message: error.toString()), - AsyncLoaded(:final value) => - value.isComplete - ? CompletedActivitySessionView(state.value, controller) - : _VocabActivityView(controller), - _ => loading, - }; - }, + child: MaxWidthBody( + withScrolling: false, + showBorder: false, + child: ValueListenableBuilder( + valueListenable: controller.sessionState, + builder: (context, state, __) { + return switch (state) { + AsyncError(:final error) => + ErrorIndicator(message: error.toString()), + AsyncLoaded(:final value) => + value.isComplete + ? CompletedActivitySessionView(state.value, controller) + : _VocabActivityView(controller), + _ => loading, + }; + }, + ), ), ), ); From 0932d0c535ab6efcdd67553fb93296fe04c4403b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 11:59:48 -0500 Subject: [PATCH 6/7] feat: backoff after failed igc and tokens requests in message sending flow --- .../choreographer/choreo_constants.dart | 1 + lib/pangea/choreographer/choreographer.dart | 43 +++++++++++++++++-- .../choreographer/igc/igc_controller.dart | 5 ++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/pangea/choreographer/choreo_constants.dart b/lib/pangea/choreographer/choreo_constants.dart index 85b9e6186..a303266ce 100644 --- a/lib/pangea/choreographer/choreo_constants.dart +++ b/lib/pangea/choreographer/choreo_constants.dart @@ -11,4 +11,5 @@ class ChoreoConstants { static const int msBeforeIGCStart = 10000; static const int maxLength = 1000; static const String inputTransformTargetKey = 'input_text_field'; + static const int defaultErrorBackoffSeconds = 5; } diff --git a/lib/pangea/choreographer/choreographer.dart b/lib/pangea/choreographer/choreographer.dart index 52348dea6..a04c1e738 100644 --- a/lib/pangea/choreographer/choreographer.dart +++ b/lib/pangea/choreographer/choreographer.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart'; @@ -45,6 +47,11 @@ class Choreographer extends ChangeNotifier { String? _lastChecked; ChoreoModeEnum _choreoMode = ChoreoModeEnum.igc; + DateTime? _lastIgcError; + DateTime? _lastTokensError; + int _igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + int _tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + StreamSubscription? _languageSub; StreamSubscription? _settingsUpdateSub; StreamSubscription? _acceptedContinuanceSub; @@ -68,6 +75,12 @@ class Choreographer extends ChangeNotifier { openMatches: [], ); + bool _backoffRequest(DateTime? error, int backoffSeconds) { + if (error == null) return false; + final secondsSinceError = DateTime.now().difference(error).inSeconds; + return secondsSinceError <= backoffSeconds; + } + void _initialize() { textController = PangeaTextController(choreographer: this); textController.addListener(_onChange); @@ -82,7 +95,14 @@ class Choreographer extends ChangeNotifier { itController.editing.addListener(_onSubmitSourceTextEdits); igcController = IgcController( - (e) => errorService.setErrorAndLock(ChoreoError(raw: e)), + (e) { + errorService.setErrorAndLock(ChoreoError(raw: e)); + _lastIgcError = DateTime.now(); + _igcErrorBackoff *= 2; + }, + () { + _igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + }, ); _languageSub ??= MatrixState @@ -233,7 +253,8 @@ class Choreographer extends ChangeNotifier { !ToolSetting.interactiveTranslator.enabled) || (!ToolSetting.autoIGC.enabled && !manual && - _choreoMode != ChoreoModeEnum.it)) { + _choreoMode != ChoreoModeEnum.it) || + _backoffRequest(_lastIgcError, _igcErrorBackoff)) { return; } @@ -275,7 +296,9 @@ class Choreographer extends ChangeNotifier { MatrixState.pangeaController.userController.userL2?.langCode; final l1LangCode = MatrixState.pangeaController.userController.userL1?.langCode; - if (l1LangCode != null && l2LangCode != null) { + if (l1LangCode != null && + l2LangCode != null && + !_backoffRequest(_lastTokensError, _tokenErrorBackoff)) { final res = await TokensRepo.get( MatrixState.pangeaController.userController.accessToken, TokensRequestModel( @@ -283,7 +306,21 @@ class Choreographer extends ChangeNotifier { senderL1: l1LangCode, senderL2: l2LangCode, ), + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + return Result.error("Token request timed out"); + }, ); + + if (res.isError) { + _lastTokensError = DateTime.now(); + _tokenErrorBackoff *= 2; + } else { + // reset backoff on success + _tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds; + } + tokensResp = res.isValue ? res.result : null; } diff --git a/lib/pangea/choreographer/igc/igc_controller.dart b/lib/pangea/choreographer/igc/igc_controller.dart index afe661ff9..9b78cb439 100644 --- a/lib/pangea/choreographer/igc/igc_controller.dart +++ b/lib/pangea/choreographer/igc/igc_controller.dart @@ -18,7 +18,8 @@ import 'package:fluffychat/widgets/matrix.dart'; class IgcController { final Function(Object) onError; - IgcController(this.onError); + final VoidCallback onFetch; + IgcController(this.onError, this.onFetch); bool _isFetching = false; String? _currentText; @@ -321,6 +322,8 @@ class IgcController { onError(res.asError!); clear(); return; + } else { + onFetch(); } if (!_isFetching) return; From 73f19471897bd66536731ffb0888108bf6de9266 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 12:07:03 -0500 Subject: [PATCH 7/7] fix: only show you in left chat message is user is the current logged in user --- lib/pages/chat/events/state_message.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index d55f286b1..2975c8a06 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -29,8 +29,10 @@ class StateMessage extends StatelessWidget { // event.calcLocalizedBodyFallback( // MatrixLocals(L10n.of(context)), // ), - event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.leave + (event.type == EventTypes.RoomMember) && + (event.roomMemberChangeType == + RoomMemberChangeType.leave) && + (event.stateKey == event.room.client.userID) ? L10n.of(context).youLeftTheChat : event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)),