From 055980005a2eb7edee2d3d833781b362e8040863 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 2 Jul 2025 11:38:30 -0400 Subject: [PATCH 1/9] When level summary is loading, show loading instead of 0 xp --- .../level_up/level_up_popup.dart | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) 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..b061f4e2c 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -500,15 +500,24 @@ class _LevelUpPopupContentState extends State color: Theme.of(context).colorScheme.onSurface, ), const SizedBox(height: 4), - Text( - '+ ${_getSkillXP(skill)} XP', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConfig.gold, - ), - textAlign: TextAlign.center, - ), + (_constructSummary != null) + ? Text( + '+ ${_getSkillXP(skill)} XP', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, + ), + textAlign: TextAlign.center, + ) + : const SizedBox( + height: 6.0, + width: 6.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.gold, + ), + ), ], ), ); From 540c7b01f0c11a733368077edaf4792a9b6fca66 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 2 Jul 2025 12:47:00 -0400 Subject: [PATCH 2/9] chore: move construct summary loading into widget --- .../get_analytics_controller.dart | 149 ++++++------- .../level_up/level_up_banner.dart | 21 +- .../level_up/level_up_manager.dart | 35 ---- .../level_up/level_up_popup.dart | 197 +++++++++--------- 4 files changed, 188 insertions(+), 214 deletions(-) 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; + 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; - } + // 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 b061f4e2c..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: () { @@ -500,24 +512,15 @@ class _LevelUpPopupContentState extends State color: Theme.of(context).colorScheme.onSurface, ), const SizedBox(height: 4), - (_constructSummary != null) - ? Text( - '+ ${_getSkillXP(skill)} XP', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConfig.gold, - ), - textAlign: TextAlign.center, - ) - : const SizedBox( - height: 6.0, - width: 6.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.gold, - ), - ), + Text( + '+ ${_getSkillXP(skill)} XP', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, + ), + textAlign: TextAlign.center, + ), ], ), ); From ad877bfd43b7b924034645b59d6d3e5f5dde3f93 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 2 Jul 2025 13:03:28 -0400 Subject: [PATCH 3/9] chore: fix autoplay bot messages on android --- lib/pages/chat/chat.dart | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index bc820b81a..1b6ad5331 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -16,6 +16,7 @@ import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -506,12 +507,21 @@ class ChatController extends State ); if (audioFile == null) return; - matrix.audioPlayer!.setAudioSource( - BytesAudioSource( - audioFile.bytes, - audioFile.mimeType, - ), - ); + if (!kIsWeb) { + final tempDir = await getTemporaryDirectory(); + + File? file; + file = File('${tempDir.path}/${audioFile.name}'); + await file.writeAsBytes(audioFile.bytes); + matrix.audioPlayer!.setFilePath(file.path); + } else { + matrix.audioPlayer!.setAudioSource( + BytesAudioSource( + audioFile.bytes, + audioFile.mimeType, + ), + ); + } matrix.audioPlayer!.play(); }); From 9250d5681f051aebadefe5787884e1c4f56a49d8 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 2 Jul 2025 13:15:24 -0400 Subject: [PATCH 4/9] chore: account for column width when calculating maxWidth for message bubble with extra content --- lib/pangea/toolbar/widgets/overlay_message.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index e678ac1ad..429dc6bb1 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -158,7 +158,10 @@ class OverlayMessage extends StatelessWidget { FluffyThemes.columnWidth * 1.5, MediaQuery.of(context).size.width - (ownMessage ? 0 : Avatar.defaultSize) - - 24.0, + 32.0 - + (FluffyThemes.isColumnMode(context) + ? FluffyThemes.columnWidth + FluffyThemes.navRailWidth + : 0.0), ), ), child: Padding( @@ -243,7 +246,10 @@ class OverlayMessage extends StatelessWidget { FluffyThemes.columnWidth * 1.5, MediaQuery.of(context).size.width - (ownMessage ? 0 : Avatar.defaultSize) - - 24.0, + 32.0 - + (FluffyThemes.isColumnMode(context) + ? FluffyThemes.columnWidth + FluffyThemes.navRailWidth + : 0.0), ), ), child: Padding( From c46236bb6cf69059bc7635d2287b8d8554d91ad0 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Wed, 2 Jul 2025 13:54:11 -0400 Subject: [PATCH 5/9] Limit space taken up by chat search result's sender name --- .../chat_search/chat_search_message_tab.dart | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index abf36f0a3..70fa9da95 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -143,15 +143,26 @@ class _MessageSearchResultListTile extends StatelessWidget { size: 16, ), const SizedBox(width: 8), - Text( - displayname, - ), - Expanded( - child: Text( - ' | ${event.originServerTs.localizedTimeShort(context)}', - style: const TextStyle(fontSize: 12), + // #Pangea + Flexible( + child: + // Pangea# + Text( + displayname, + // #Pangea + maxLines: 1, + overflow: TextOverflow.ellipsis, + // Pangea# ), ), + // #Pangea + // Expanded( + // child: + // Pangea# + Text( + ' | ${event.originServerTs.localizedTimeShort(context)}', + style: const TextStyle(fontSize: 12), + ), ], ), subtitle: Linkify( From 8520e3d59ec098a4dfda340fbf5648d64166d692 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 2 Jul 2025 14:47:28 -0400 Subject: [PATCH 6/9] chore: simplify pangea comments --- .../chat_search/chat_search_message_tab.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index 70fa9da95..5305244c3 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -144,25 +144,27 @@ class _MessageSearchResultListTile extends StatelessWidget { ), const SizedBox(width: 8), // #Pangea + // Text( + // displayname, + // ), + // Expanded( + // child: Text( + // ' | ${event.originServerTs.localizedTimeShort(context)}', + // style: const TextStyle(fontSize: 12), + // ), + // ), Flexible( - child: - // Pangea# - Text( + child: Text( displayname, - // #Pangea maxLines: 1, overflow: TextOverflow.ellipsis, - // Pangea# ), ), - // #Pangea - // Expanded( - // child: - // Pangea# Text( ' | ${event.originServerTs.localizedTimeShort(context)}', style: const TextStyle(fontSize: 12), ), + // Pangea# ], ), subtitle: Linkify( From aeaa4321d2b6393cd592b66bf4ea9a0fc53fe5b1 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:29:14 -0400 Subject: [PATCH 7/9] chore: decrease and stop confetti a few seconds after level up --- .../analytics_misc/level_up/rain_confetti.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart index 13e43aca3..d7b4fbf81 100644 --- a/lib/pangea/analytics_misc/level_up/rain_confetti.dart +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -12,9 +12,15 @@ ConfettiController? _rainController; void rainConfetti(BuildContext context) { if (_confettiEntry != null) return; // Prevent duplicates + int numParticles = 2; _blastController = ConfettiController(duration: const Duration(seconds: 1)); - _rainController = ConfettiController(duration: const Duration(seconds: 3)); + _rainController = ConfettiController(duration: const Duration(seconds: 8)); + Future.delayed(const Duration(seconds: 4), () { + if (_rainController!.state == ConfettiControllerState.playing) { + numParticles = 1; + } + }); _blastController!.play(); _rainController!.play(); @@ -61,14 +67,14 @@ void rainConfetti(BuildContext context) { confettiController: _rainController!, blastDirectionality: BlastDirectionality.directional, blastDirection: 3 * pi / 2, - shouldLoop: true, + shouldLoop: false, maxBlastForce: 5, minBlastForce: 2, minimumSize: const Size(20, 20), maximumSize: const Size(25, 25), gravity: 0.07, emissionFrequency: 0.1, - numberOfParticles: 2, + numberOfParticles: numParticles, colors: const [AppConfig.goldLight, AppConfig.gold], createParticlePath: drawStar, ), From 6166511e1086b2e9fdd2a5cddaaecbc428c90871 Mon Sep 17 00:00:00 2001 From: avashilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:44:21 -0400 Subject: [PATCH 8/9] small change to avoid null reference --- lib/pangea/analytics_misc/level_up/rain_confetti.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart index d7b4fbf81..4dc7cb968 100644 --- a/lib/pangea/analytics_misc/level_up/rain_confetti.dart +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -17,7 +17,7 @@ void rainConfetti(BuildContext context) { _blastController = ConfettiController(duration: const Duration(seconds: 1)); _rainController = ConfettiController(duration: const Duration(seconds: 8)); Future.delayed(const Duration(seconds: 4), () { - if (_rainController!.state == ConfettiControllerState.playing) { + if (_rainController?.state == ConfettiControllerState.playing) { numParticles = 1; } }); From 44982e8b846a0eecf3fa57adb6b8461a56615f35 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 2 Jul 2025 15:56:27 -0400 Subject: [PATCH 9/9] chore: updates to activity generator UI --- lib/l10n/intl_en.arb | 7 +- lib/l10n/intl_es.arb | 2 - lib/l10n/intl_vi.arb | 2 - .../activity_generator.dart | 30 +--- .../activity_generator_view.dart | 1 + .../activity_planner/activity_plan_card.dart | 158 +++++++++++++----- .../activity_plan_generation_repo.dart | 7 +- .../activity_planner_builder.dart | 17 +- .../bookmarked_activity_list.dart | 10 -- 9 files changed, 135 insertions(+), 99 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3f07c4cce..bcd8868b7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4732,7 +4732,7 @@ "activityPlannerTitle": "Activity Planner", "topicLabel": "Topic", "topicPlaceholder": "Choose a topic...", - "modeLabel": "Mode", + "modeLabel": "Activity type", "modePlaceholder": "Choose a mode...", "learningObjectiveLabel": "Learning Objective", "learningObjectivePlaceholder": "Choose a learning objective...", @@ -4873,7 +4873,7 @@ "exploreMore": "Explore more", "randomize": "Randomize", "clear": "Clear", - "makeYourOwnActivity": "Make your own activity", + "makeYourOwnActivity": "Create your own activity", "featuredActivities": "Featured", "yourBookmarks": "Bookmarked", "goToChat": "Go to chat", @@ -5031,5 +5031,6 @@ } }, "failedToFetchTranscription": "Failed to fetch transcription", - "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone." + "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone.", + "regenerate": "Regenerate" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index e1407a289..2dd1d6672 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -5362,7 +5362,6 @@ "activityPlannerTitle": "Planificador de Actividades", "topicLabel": "Tema", "topicPlaceholder": "Elige un tema...", - "modeLabel": "Modo", "modePlaceholder": "Elige un modo...", "learningObjectiveLabel": "Objetivo de Aprendizaje", "learningObjectivePlaceholder": "Elige un objetivo de aprendizaje...", @@ -5494,7 +5493,6 @@ "exploreMore": "Explorar más", "randomize": "Aleatorizar", "clear": "Limpiar", - "makeYourOwnActivity": "Crea tu propia actividad", "featuredActivities": "Destacadas", "yourBookmarks": "Marcados", "goToChat": "Ir al chat", diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 87d240b4d..42268e099 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3551,7 +3551,6 @@ "activityPlannerTitle": "Trình lập hoạt động", "topicLabel": "Chủ đề", "topicPlaceholder": "Chọn một chủ đề...", - "modeLabel": "Chế độ", "modePlaceholder": "Chọn một chế độ...", "learningObjectiveLabel": "Mục tiêu học tập", "learningObjectivePlaceholder": "Chọn một mục tiêu học tập...", @@ -3834,7 +3833,6 @@ "exploreMore": "Khám phá thêm", "randomize": "Ngẫu nhiên hóa", "clear": "Xóa", - "makeYourOwnActivity": "Tạo hoạt động của riêng bạn", "featuredActivities": "Nổi bật", "yourBookmarks": "Đã đánh dấu", "goToChat": "Đi đến trò chuyện", diff --git a/lib/pangea/activity_generator/activity_generator.dart b/lib/pangea/activity_generator/activity_generator.dart index 2ed3dcd9b..2f09675ce 100644 --- a/lib/pangea/activity_generator/activity_generator.dart +++ b/lib/pangea/activity_generator/activity_generator.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart' import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart'; import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart'; import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart'; import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; @@ -166,11 +165,6 @@ class ActivityGeneratorState extends State { setState(() => selectedCefrLevel = value); } - void setSelectedMedia(MediaEnum? value) { - if (value == null) return; - setState(() => selectedMedia = value); - } - Future get _selectedMode async { final modes = await modeItems; return modes.firstWhereOrNull( @@ -203,30 +197,18 @@ class ActivityGeneratorState extends State { }); } - Future onEdit(int index, ActivityPlanModel updatedActivity) async { - // in this case we're editing an activity plan that was generated recently - // via the repo and should be updated in the cached response - if (activities != null) { - activities?[index] = updatedActivity; - ActivityPlanGenerationRepo.set( - planRequest, - ActivityPlanResponse(activityPlans: activities!), - ); - } - - setState(() {}); - } - - void update() => setState(() {}); - - Future generate() async { + Future generate({bool force = false}) async { setState(() { loading = true; error = null; + activities = null; }); try { - final resp = await ActivityPlanGenerationRepo.get(planRequest); + final resp = await ActivityPlanGenerationRepo.get( + planRequest, + force: force, + ); activities = resp.activityPlans; await _setModeImageURL(); } catch (e, s) { diff --git a/lib/pangea/activity_generator/activity_generator_view.dart b/lib/pangea/activity_generator/activity_generator_view.dart index 862d523ae..b3c74551e 100644 --- a/lib/pangea/activity_generator/activity_generator_view.dart +++ b/lib/pangea/activity_generator/activity_generator_view.dart @@ -61,6 +61,7 @@ class ActivityGeneratorView extends StatelessWidget { room: controller.room, builder: (c) { return ActivityPlanCard( + regenerate: () => controller.generate(force: true), controller: c, ); }, diff --git a/lib/pangea/activity_planner/activity_plan_card.dart b/lib/pangea/activity_planner/activity_plan_card.dart index 68ed306a3..ce0a9546c 100644 --- a/lib/pangea/activity_planner/activity_plan_card.dart +++ b/lib/pangea/activity_planner/activity_plan_card.dart @@ -20,10 +20,12 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en import 'package:fluffychat/widgets/future_loading_dialog.dart'; class ActivityPlanCard extends StatefulWidget { + final VoidCallback regenerate; final ActivityPlannerBuilderState controller; const ActivityPlanCard({ super.key, + required this.regenerate, required this.controller, }); @@ -121,8 +123,11 @@ class ActivityPlanCardState extends State { AnimatedSize( duration: FluffyThemes.animationDuration, child: Stack( + alignment: Alignment.bottomCenter, children: [ Container( + width: 200.0, + padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0), ), @@ -131,6 +136,7 @@ class ActivityPlanCardState extends State { child: widget.controller.imageURL != null || widget.controller.avatar != null ? ClipRRect( + borderRadius: BorderRadius.circular(20.0), child: widget.controller.avatar == null ? CachedNetworkImage( fit: BoxFit.cover, @@ -156,14 +162,17 @@ class ActivityPlanCardState extends State { ), ), if (widget.controller.isEditing) - Positioned( - top: 10.0, - right: 10.0, - child: IconButton( - icon: const Icon(Icons.upload_outlined), - onPressed: widget.controller.selectAvatar, - style: IconButton.styleFrom( - backgroundColor: Colors.black, + InkWell( + borderRadius: BorderRadius.circular(90), + onTap: widget.controller.selectAvatar, + child: CircleAvatar( + backgroundColor: + Theme.of(context).colorScheme.secondary, + radius: 16.0, + child: Icon( + Icons.add_a_photo_outlined, + size: 16.0, + color: Theme.of(context).colorScheme.onSecondary, ), ), ), @@ -368,47 +377,108 @@ class ActivityPlanCardState extends State { ), ], const SizedBox(height: itemPadding), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Tooltip( - message: !widget.controller.isEditing - ? l10n.edit - : l10n.saveChanges, - child: IconButton( - icon: Icon( - !widget.controller.isEditing - ? Icons.edit - : Icons.save, + widget.controller.isEditing + ? Row( + spacing: 12.0, + children: [ + Expanded( + child: ElevatedButton( + onPressed: widget.controller.saveEdits, + child: Row( + children: [ + const Icon(Icons.save), + Expanded( + child: Text( + L10n.of(context).save, + textAlign: TextAlign.center, + ), + ), + ], + ), ), - onPressed: () => !widget.controller.isEditing - ? setState(() { - widget.controller.isEditing = true; - }) - : widget.controller.saveEdits(), - isSelected: widget.controller.isEditing, ), - ), - if (widget.controller.isEditing) - Tooltip( - message: l10n.cancel, - child: IconButton( - icon: const Icon(Icons.cancel), + Expanded( + child: ElevatedButton( onPressed: widget.controller.clearEdits, + child: Row( + children: [ + const Icon(Icons.cancel), + Expanded( + child: Text( + L10n.of(context).cancel, + textAlign: TextAlign.center, + ), + ), + ], + ), ), ), - ], - ), - ElevatedButton.icon( - onPressed: - !widget.controller.isEditing ? _onLaunch : null, - icon: const Icon(Icons.send), - label: Text(l10n.launchActivityButton), - ), - ], - ), + ], + ) + : Column( + spacing: 12.0, + children: [ + Row( + spacing: 12.0, + children: [ + Expanded( + child: ElevatedButton( + child: Row( + children: [ + const Icon(Icons.edit), + Expanded( + child: Text( + L10n.of(context).edit, + textAlign: TextAlign.center, + ), + ), + ], + ), + onPressed: () => + widget.controller.setEditing(true), + ), + ), + Expanded( + child: ElevatedButton( + onPressed: widget.regenerate, + child: Row( + children: [ + const Icon(Icons.lightbulb_outline), + Expanded( + child: Text( + L10n.of(context).regenerate, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: _onLaunch, + child: Row( + children: [ + const Icon(Icons.send), + Expanded( + child: Text( + L10n.of(context) + .launchActivityButton, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), ], ), ), diff --git a/lib/pangea/activity_planner/activity_plan_generation_repo.dart b/lib/pangea/activity_planner/activity_plan_generation_repo.dart index 70e079e85..261b41a20 100644 --- a/lib/pangea/activity_planner/activity_plan_generation_repo.dart +++ b/lib/pangea/activity_planner/activity_plan_generation_repo.dart @@ -18,9 +18,12 @@ class ActivityPlanGenerationRepo { _activityPlanStorage.write(request.storageKey, response.toJson()); } - static Future get(ActivityPlanRequest request) async { + static Future get( + ActivityPlanRequest request, { + bool force = false, + }) async { final cachedJson = _activityPlanStorage.read(request.storageKey); - if (cachedJson != null) { + if (cachedJson != null && !force) { final cached = ActivityPlanResponse.fromJson(cachedJson); return cached; diff --git a/lib/pangea/activity_planner/activity_planner_builder.dart b/lib/pangea/activity_planner/activity_planner_builder.dart index 42e84bce2..d2ec4bdf2 100644 --- a/lib/pangea/activity_planner/activity_planner_builder.dart +++ b/lib/pangea/activity_planner/activity_planner_builder.dart @@ -7,6 +7,7 @@ import 'package:http/http.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; @@ -21,18 +22,12 @@ class ActivityPlannerBuilder extends StatefulWidget { final Widget Function(ActivityPlannerBuilderState) builder; - final Future Function( - String, - ActivityPlanModel, - )? onEdit; - const ActivityPlannerBuilder({ super.key, required this.initialActivity, this.initialFilename, this.room, required this.builder, - this.onEdit, }); @override @@ -206,12 +201,10 @@ class ActivityPlannerBuilderState extends State { if (!formKey.currentState!.validate()) return; await updateImageURL(); setEditing(false); - if (widget.onEdit != null) { - await widget.onEdit!( - widget.initialActivity.bookmarkId, - updatedActivity, - ); - } + + await BookmarkedActivitiesRepo.remove(widget.initialActivity.bookmarkId); + await BookmarkedActivitiesRepo.save(updatedActivity); + if (mounted) setState(() {}); } Future clearEdits() async { diff --git a/lib/pangea/activity_planner/bookmarked_activity_list.dart b/lib/pangea/activity_planner/bookmarked_activity_list.dart index e11bf9f0b..241a162ef 100644 --- a/lib/pangea/activity_planner/bookmarked_activity_list.dart +++ b/lib/pangea/activity_planner/bookmarked_activity_list.dart @@ -36,15 +36,6 @@ class BookmarkedActivitiesListState extends State { double get cardHeight => _isColumnMode ? 325.0 : 250.0; double get cardWidth => _isColumnMode ? 225.0 : 150.0; - Future _onEdit( - String activityId, - ActivityPlanModel activity, - ) async { - await BookmarkedActivitiesRepo.remove(activityId); - await BookmarkedActivitiesRepo.save(activity); - if (mounted) setState(() {}); - } - @override Widget build(BuildContext context) { final l10n = L10n.of(context); @@ -77,7 +68,6 @@ class BookmarkedActivitiesListState extends State { builder: (context) { return ActivityPlannerBuilder( initialActivity: activity, - onEdit: _onEdit, room: widget.room, builder: (controller) { return ActivitySuggestionDialog(