diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index 5d861b089..e296a0c2e 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -474,100 +474,87 @@ class GetAnalyticsController extends BaseController { } } - Future generateLevelUpAnalytics( + Future generateLevelUpAnalytics( final int lowerLevel, final int upperLevel, ) async { - // generate level up analytics as a construct summary - ConstructSummary summary; - try { - final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); - final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); - int diffXP = maxXP - minXP; - if (diffXP < 0) diffXP = 0; - - // compute construct use of current level - final List constructUseOfCurrentLevel = []; - int score = 0; - for (final use in constructListModel.uses) { - constructUseOfCurrentLevel.add(use); - score += use.xp; - if (score >= diffXP) break; - } + final int maxXP = constructListModel.calculateXpWithLevel(upperLevel); + final int minXP = constructListModel.calculateXpWithLevel(lowerLevel); + int diffXP = maxXP - minXP; + if (diffXP < 0) diffXP = 0; + + // compute construct use of current level + final List constructUseOfCurrentLevel = []; + int score = 0; + for (final use in constructListModel.uses) { + constructUseOfCurrentLevel.add(use); + score += use.xp; + if (score >= diffXP) break; + } - // extract construct use message bodies for analytics - final Map> useEventIds = {}; - for (final use in constructUseOfCurrentLevel) { - if (use.metadata.roomId == null) continue; - if (use.metadata.eventId == null) continue; - useEventIds[use.metadata.roomId!] ??= {}; - useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!); - } + // extract construct use message bodies for analytics + final Map> useEventIds = {}; + for (final use in constructUseOfCurrentLevel) { + if (use.metadata.roomId == null) continue; + if (use.metadata.eventId == null) continue; + useEventIds[use.metadata.roomId!] ??= {}; + useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!); + } - final List constructUseMessageContentBodies = []; - for (final entry in useEventIds.entries) { - final String roomId = entry.key; - final room = _client.getRoomById(roomId); - if (room == null) continue; - final List messageBodies = []; - for (final eventId in entry.value) { - try { - final Event? event = await room.getEventById(eventId); - if (event?.content["body"] is! String) continue; - final String body = event?.content["body"] as String; - if (body.isEmpty) continue; - messageBodies.add(body); - } catch (e, s) { - debugPrint("Error getting event by ID: $e"); - ErrorHandler.logError( - e: e, - s: s, - data: { - 'roomId': roomId, - 'eventId': eventId, - }, - ); - continue; - } + final List constructUseMessageContentBodies = []; + for (final entry in useEventIds.entries) { + final String roomId = entry.key; + final room = _client.getRoomById(roomId); + if (room == null) continue; + final List messageBodies = []; + for (final eventId in entry.value) { + try { + final Event? event = await room.getEventById(eventId); + if (event?.content["body"] is! String) continue; + final String body = event?.content["body"] as String; + if (body.isEmpty) continue; + messageBodies.add(body); + } catch (e, s) { + debugPrint("Error getting event by ID: $e"); + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': roomId, + 'eventId': eventId, + }, + ); + continue; } - constructUseMessageContentBodies.addAll(messageBodies); } - - final request = ConstructSummaryRequest( - constructs: constructUseOfCurrentLevel, - constructUseMessageContentBodies: constructUseMessageContentBodies, - language: _l1!.langCodeShort, - upperLevel: upperLevel, - lowerLevel: lowerLevel, - ); - - final response = await ConstructRepo.generateConstructSummary(request); - summary = response.summary; - summary.levelVocabConstructs = MatrixState - .pangeaController.getAnalytics.constructListModel.vocabLemmas; - summary.levelGrammarConstructs = MatrixState - .pangeaController.getAnalytics.constructListModel.grammarLemmas; - } catch (e) { - debugPrint("Error generating level up analytics: $e"); - ErrorHandler.logError(e: e, data: {'e': e}); - return null; + constructUseMessageContentBodies.addAll(messageBodies); } - try { - final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!); - if (analyticsRoom == null) { - throw "Analytics room not found for user"; - } + final request = ConstructSummaryRequest( + constructs: constructUseOfCurrentLevel, + constructUseMessageContentBodies: constructUseMessageContentBodies, + language: _l1!.langCodeShort, + upperLevel: upperLevel, + lowerLevel: lowerLevel, + ); - // don't await this, just return the original response - _saveConstructSummaryResponseToStateEvent( - summary, - ); - } catch (e, s) { - debugPrint("Error saving construct summary room: $e"); - ErrorHandler.logError(e: e, s: s, data: {'e': e}); + final response = await ConstructRepo.generateConstructSummary(request); + final ConstructSummary summary = response.summary; + summary.levelVocabConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + summary.levelGrammarConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; + + final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!); + if (analyticsRoom == null) { + throw "Analytics room not found for user"; } + // don't await this, just return the original response + _saveConstructSummaryResponseToStateEvent( + summary, + ); + return summary; } } diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index a94916231..467a3f0bf 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart' import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LevelUpConstants { @@ -95,10 +96,15 @@ class LevelUpBannerState extends State bool _showedDetails = false; + final Completer _constructSummaryCompleter = + Completer(); + @override void initState() { super.initState(); + _loadConstructSummary(); + LevelUpManager.instance.preloadAnalytics( context, widget.level, @@ -149,10 +155,23 @@ class LevelUpBannerState extends State await showDialog( context: context, - builder: (context) => const LevelUpPopup(), + builder: (context) => LevelUpPopup( + constructSummaryCompleter: _constructSummaryCompleter, + ), ); } + Future _loadConstructSummary() async { + try { + final summary = MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics(widget.prevLevel, widget.level); + _constructSummaryCompleter.complete(summary); + } catch (e) { + debugPrint("Error generating level up analytics: $e"); + _constructSummaryCompleter.completeError(e); + } + } + @override Widget build(BuildContext context) { final isColumnMode = FluffyThemes.isColumnMode(context); diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart index 7716d4165..cec323475 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -22,13 +22,8 @@ class LevelUpManager { int prevVocab = 0; int nextVocab = 0; - String? userL2Code; - - ConstructSummary? constructSummary; - bool hasSeenPopup = false; bool shouldAutoPopup = false; - String? error; Future preloadAnalytics( BuildContext context, @@ -46,12 +41,6 @@ class LevelUpManager { nextVocab = MatrixState .pangeaController.getAnalytics.constructListModel.vocabLemmas; - userL2Code = MatrixState.pangeaController.languageController - .activeL2Code() - ?.toUpperCase(); - - getConstructFromLevelUp(); - final LanguageModel? l2 = MatrixState.pangeaController.languageController.userL2; final Room? analyticsRoom = @@ -91,28 +80,6 @@ class LevelUpManager { } } - //for testing, just fetch last level up from saved analytics - void getConstructFromButton() { - constructSummary = MatrixState.pangeaController.getAnalytics - .getConstructSummaryFromStateEvent(); - debugPrint( - "Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}", - ); - } - - //for getting real level up data when leveled up - void getConstructFromLevelUp() async { - try { - constructSummary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics( - prevLevel, - level, - ); - } catch (e) { - error = e.toString(); - } - } - void markPopupSeen() { hasSeenPopup = true; shouldAutoPopup = false; @@ -127,7 +94,5 @@ class LevelUpManager { nextGrammar = 0; prevVocab = 0; nextVocab = 0; - constructSummary = null; - error = null; } } diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart index c7aa2fb3b..795700652 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -20,12 +20,15 @@ import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart' import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; class LevelUpPopup extends StatelessWidget { + final Completer constructSummaryCompleter; const LevelUpPopup({ + required this.constructSummaryCompleter, super.key, }); @@ -50,6 +53,7 @@ class LevelUpPopup extends StatelessWidget { body: LevelUpPopupContent( prevLevel: LevelUpManager.instance.prevLevel, level: LevelUpManager.instance.level, + constructSummaryCompleter: constructSummaryCompleter, ), ), ); @@ -59,11 +63,13 @@ class LevelUpPopup extends StatelessWidget { class LevelUpPopupContent extends StatefulWidget { final int prevLevel; final int level; + final Completer constructSummaryCompleter; const LevelUpPopupContent({ super.key, required this.prevLevel, required this.level, + required this.constructSummaryCompleter, }); @override @@ -72,55 +78,39 @@ class LevelUpPopupContent extends StatefulWidget { class _LevelUpPopupContentState extends State with SingleTickerProviderStateMixin { - late int _endGrammar; - late int _endVocab; - final int _startGrammar = LevelUpManager.instance.prevGrammar; - final int _startVocab = LevelUpManager.instance.prevVocab; - Timer? _summaryPollTimer; - final String? _error = LevelUpManager.instance.error; - String language = LevelUpManager.instance.userL2Code ?? "N/A"; - late final AnimationController _controller; late final ConfettiController _confettiController; - bool _hasBlastedConfetti = false; - final Duration _animationDuration = const Duration(seconds: 5); - - Uri? avatarUrl; late final Future profile; + int displayedLevel = -1; - late ConstructSummary? _constructSummary; + Uri? avatarUrl; + bool _hasBlastedConfetti = false; + + String language = MatrixState.pangeaController.languageController + .activeL2Code() + ?.toUpperCase() ?? + LanguageKeys.unknownLanguage; + + ConstructSummary? _constructSummary; + Object? _error; + bool _loading = true; @override void initState() { super.initState(); + _loadConstructSummary(); LevelUpManager.instance.markPopupSeen(); displayedLevel = widget.prevLevel; _confettiController = ConfettiController(duration: const Duration(seconds: 1)); - _endGrammar = LevelUpManager.instance.nextGrammar; - _endVocab = LevelUpManager.instance.nextVocab; - _constructSummary = LevelUpManager.instance.constructSummary; - // Poll for constructSummary if not available - if (_constructSummary == null) { - _summaryPollTimer = - Timer.periodic(const Duration(milliseconds: 300), (timer) { - final summary = LevelUpManager.instance.constructSummary; - if (summary != null) { - setState(() { - _constructSummary = summary; - }); - timer.cancel(); - } - }); - } + final client = Matrix.of(context).client; client.fetchOwnProfile().then((profile) { - setState(() { - avatarUrl = profile.avatarUrl; - }); + setState(() => avatarUrl = profile.avatarUrl); }); + _controller = AnimationController( - duration: _animationDuration, + duration: const Duration(seconds: 5), vsync: this, ); @@ -135,7 +125,6 @@ class _LevelUpPopupContentState extends State _controller.addListener(() { if (_controller.value >= 0.5 && !_hasBlastedConfetti) { - //_confettiController.play(); _hasBlastedConfetti = true; rainConfetti(context); } @@ -146,7 +135,6 @@ class _LevelUpPopupContentState extends State @override void dispose() { - _summaryPollTimer?.cancel(); _controller.dispose(); _confettiController.dispose(); LevelUpManager.instance.reset(); @@ -154,6 +142,22 @@ class _LevelUpPopupContentState extends State super.dispose(); } + int get _startGrammar => LevelUpManager.instance.prevGrammar; + int get _startVocab => LevelUpManager.instance.prevVocab; + + get _endGrammar => LevelUpManager.instance.nextGrammar; + get _endVocab => LevelUpManager.instance.nextVocab; + + Future _loadConstructSummary() async { + try { + _constructSummary = await widget.constructSummaryCompleter.future; + } catch (e) { + _error = e; + } finally { + setState(() => _loading = false); + } + } + int _getSkillXP(LearningSkillsEnum skill) { if (_constructSummary == null) return 0; return switch (skill) { @@ -368,52 +372,60 @@ class _LevelUpPopupContentState extends State ), ), const SizedBox(height: 16), - - // Skills section - AnimatedBuilder( - animation: skillsOpacity, - builder: (_, __) => Opacity( - opacity: skillsOpacity.value, - child: _error == null - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildSkillsTable(context), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - _constructSummary?.textSummary ?? - L10n.of(context).loadingPleaseWait, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - ), - ], - ) - // if error getting construct summary - : Row( - children: [ - Tooltip( - message: L10n.of(context).oopsSomethingWentWrong, - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - ), - ], + if (_loading) + const Center( + child: SizedBox( + height: 50, + width: 50, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.goldLight, + ), + ), + ) + else if (_error != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context).oopsSomethingWentWrong, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 16, + ), + ), + ) + else if (_constructSummary != null) + // Skills section + AnimatedBuilder( + animation: skillsOpacity, + builder: (_, __) => Opacity( + opacity: skillsOpacity.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary!.textSummary, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), ), + ], + ), + ), ), - ), // Share button, currently no functionality // ElevatedButton( // onPressed: () {