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 014816ee0..1434c29f9 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -114,12 +114,9 @@ class VocabDetailsView extends StatelessWidget { ], ), const SizedBox(height: 16.0), - LemmmaHighlightEmojiRow( + LemmaHighlightEmojiRow( controller: controller, - isSelected: false, cId: constructId, - onTapOverride: null, - iconSize: _iconSize, ), ], ), diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart index cbfd70ef1..331d56fee 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_list_view.dart @@ -161,8 +161,7 @@ class VocabAnalyticsListView extends StatelessWidget { "/rooms/analytics/${vocabItem.id.type.string}/${vocabItem.id.string}", ), constructUse: vocabItem, - emoji: vocabItem.id.userSetEmoji.firstOrNull ?? - vocabItem.id.getLemmaInfoCached()?.emoji.firstOrNull, + emoji: vocabItem.id.userSetEmoji.firstOrNull, ); }, childCount: _filteredVocab.length, diff --git a/lib/pangea/analytics_page/analytics_page.dart b/lib/pangea/analytics_page/analytics_page.dart index cc952fa94..d1efb4bd0 100644 --- a/lib/pangea/analytics_page/analytics_page.dart +++ b/lib/pangea/analytics_page/analytics_page.dart @@ -15,7 +15,7 @@ import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dar import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class AnalyticsPage extends StatelessWidget { +class AnalyticsPage extends StatefulWidget { final ProgressIndicatorEnum? indicator; final ConstructIdentifier? construct; final bool isSidebar; @@ -27,62 +27,81 @@ class AnalyticsPage extends StatelessWidget { this.isSidebar = false, }); + @override + State createState() => _AnalyticsPageState(); +} + +class _AnalyticsPageState extends State { + @override + void initState() { + super.initState(); + final analytics = MatrixState.pangeaController.getAnalytics; + + // Check if getAnalytics is initialized, if not wait for the first stream entry + if (!analytics.initCompleter.isCompleted) { + analytics.analyticsStream.stream.first.then((_) { + if (mounted) { + setState(() {}); + } + }); + } + } + @override Widget build(BuildContext context) { final analyticsRoomId = GoRouterState.of(context).pathParameters['roomid']; return Scaffold( - appBar: construct != null ? AppBar() : null, + appBar: widget.construct != null ? AppBar() : null, body: SafeArea( child: Padding( padding: const EdgeInsetsGeometry.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isSidebar || - (!FluffyThemes.isColumnMode(context) && construct == null)) + if (widget.isSidebar || + (!FluffyThemes.isColumnMode(context) && + widget.construct == null)) LearningProgressIndicators( - selected: indicator, - canSelect: indicator != ProgressIndicatorEnum.level, + selected: widget.indicator, + canSelect: widget.indicator != ProgressIndicatorEnum.level, ), Expanded( - child: StreamBuilder( - stream: MatrixState - .pangeaController.getAnalytics.analyticsStream.stream, - builder: (context, _) { - if (indicator == ProgressIndicatorEnum.level) { - return const LevelDialogContent(); - } else if (indicator == ProgressIndicatorEnum.morphsUsed) { - return ConstructAnalyticsView( - construct: construct, - view: ConstructTypeEnum.morph, - ); - } else if (indicator == ProgressIndicatorEnum.wordsUsed) { - return ConstructAnalyticsView( - construct: construct, - view: ConstructTypeEnum.vocab, - ); - } else if (indicator == ProgressIndicatorEnum.activities) { - return ActivityArchive( - selectedRoomId: analyticsRoomId, - ); - } + child: () { + if (widget.indicator == ProgressIndicatorEnum.level) { + return const LevelDialogContent(); + } else if (widget.indicator == + ProgressIndicatorEnum.morphsUsed) { + return ConstructAnalyticsView( + construct: widget.construct, + view: ConstructTypeEnum.morph, + ); + } else if (widget.indicator == + ProgressIndicatorEnum.wordsUsed) { + return ConstructAnalyticsView( + construct: widget.construct, + view: ConstructTypeEnum.vocab, + ); + } else if (widget.indicator == + ProgressIndicatorEnum.activities) { + return ActivityArchive( + selectedRoomId: analyticsRoomId, + ); + } - return Center( - child: SizedBox( - width: 250.0, - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${AnalyticsPageConstants.dinoBotFileName}", - errorWidget: (context, url, error) => - const SizedBox(), - placeholder: (context, url) => const Center( - child: CircularProgressIndicator.adaptive(), - ), + return Center( + child: SizedBox( + width: 250.0, + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${AnalyticsPageConstants.dinoBotFileName}", + errorWidget: (context, url, error) => const SizedBox(), + placeholder: (context, url) => const Center( + child: CircularProgressIndicator.adaptive(), ), ), - ); - }, - ), + ), + ); + }(), ), ], ), diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index 48148f2b9..d6f108e27 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -10,6 +10,8 @@ import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/emojis/emoji_stack.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; @@ -175,6 +177,57 @@ class ConstructIdentifier { } } + /// Sets emoji and awards XP if it's a NEW emoji selection or from game + Future setEmojiWithXP({ + required String emoji, + bool isFromCorrectAnswer = false, + String? eventId, + String? roomId, + }) async { + final hadEmojiPreviously = userSetEmoji.isNotEmpty; + //correct answers already award xp so we don't here, but we do still need to set the emoji if it isn't already set + final shouldAwardXP = !hadEmojiPreviously && !isFromCorrectAnswer; + + //Set emoji representation + await setUserLemmaInfo(UserSetLemmaInfo(emojis: [emoji])); + + if (shouldAwardXP) { + await _recordEmojiAnalytics( + eventId: eventId, + roomId: roomId, + ); + } + } + + Future _recordEmojiAnalytics({ + String? eventId, + String? roomId, + }) async { + const useType = ConstructUseTypeEnum.em; + + MatrixState.pangeaController.putAnalytics.setState( + AnalyticsStream( + eventId: eventId, + roomId: roomId, + constructs: [ + OneConstructUse( + useType: useType, + lemma: lemma, + constructType: type, + metadata: ConstructUseMetaData( + roomId: roomId, + timeStamp: DateTime.now(), + eventId: eventId, + ), + category: category, + form: lemma, + xp: useType.pointValue, + ), + ], + ), + ); + } + Future setUserLemmaInfo(UserSetLemmaInfo newLemmaInfo) async { final client = MatrixState.pangeaController.matrixState.client; final l2 = MatrixState.pangeaController.languageController.userL2; diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index 00d459d93..1e6e23e82 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -1,48 +1,65 @@ +import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; -import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; +import 'package:shimmer/shimmer.dart'; -class LemmmaHighlightEmojiRow extends StatefulWidget { +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LemmaHighlightEmojiRow extends StatefulWidget { final LemmaMeaningBuilderState controller; final ConstructIdentifier cId; - final VoidCallback? onTapOverride; - final bool isSelected; - final double? iconSize; - const LemmmaHighlightEmojiRow({ + const LemmaHighlightEmojiRow({ super.key, required this.controller, required this.cId, - required this.onTapOverride, - required this.isSelected, - this.iconSize, }); @override - LemmmaHighlightEmojiRowState createState() => LemmmaHighlightEmojiRowState(); + LemmaHighlightEmojiRowState createState() => LemmaHighlightEmojiRowState(); } -class LemmmaHighlightEmojiRowState extends State { +class LemmaHighlightEmojiRowState extends State { String? displayEmoji; + bool _showShimmer = true; + bool _hasShimmered = false; @override void initState() { super.initState(); displayEmoji = widget.cId.userSetEmoji.firstOrNull; + _showShimmer = (displayEmoji == null); + } + + void _startShimmer() { + if (!widget.controller.isLoading && _showShimmer) { + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) { + setState(() => _showShimmer = false); + setState(() => _hasShimmered = true); + } + }); + } } @override - didUpdateWidget(LemmmaHighlightEmojiRow oldWidget) { - if (oldWidget.isSelected != widget.isSelected || - widget.cId.userSetEmoji != oldWidget.cId.userSetEmoji) { - setState(() => displayEmoji = widget.cId.userSetEmoji.firstOrNull); + didUpdateWidget(LemmaHighlightEmojiRow oldWidget) { + //Reset shimmer state for diff constructs in 2 column mode + if (oldWidget.cId != widget.cId) { + setState(() { + displayEmoji = widget.cId.userSetEmoji.firstOrNull; + _showShimmer = (displayEmoji == null); + _hasShimmered = false; + }); } super.didUpdateWidget(oldWidget); } @@ -52,14 +69,33 @@ class LemmmaHighlightEmojiRowState extends State { super.dispose(); } - Future setEmoji(String emoji) async { + String transformTargetId(String emoji) => + "emoji-choice-item-$emoji-${widget.cId.lemma}"; + + Future setEmoji(String emoji, BuildContext context) async { try { + final String? userSetEmoji = widget.cId.userSetEmoji.firstOrNull; setState(() => displayEmoji = emoji); - await widget.cId.setUserLemmaInfo( - UserSetLemmaInfo( - emojis: [emoji], - ), + await widget.cId.setEmojiWithXP( + emoji: emoji, + isFromCorrectAnswer: false, ); + if (userSetEmoji == null) { + OverlayUtil.showOverlay( + overlayKey: "${transformTargetId(emoji)}_points", + followerAnchor: Alignment.bottomCenter, + targetAnchor: Alignment.bottomCenter, + context: context, + child: PointsGainedAnimation( + points: 2, + targetID: transformTargetId(emoji), + ), + transformTargetId: transformTargetId(emoji), + closePrevOverlay: false, + backDropToDismiss: false, + ignorePointer: true, + ); + } } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s); @@ -71,6 +107,7 @@ class LemmmaHighlightEmojiRowState extends State { if (widget.controller.isLoading) { return const CircularProgressIndicator.adaptive(); } + _startShimmer(); final emojis = widget.controller.lemmaInfo?.emoji; if (widget.controller.error != null || emojis == null || emojis.isEmpty) { @@ -92,10 +129,10 @@ class LemmmaHighlightEmojiRowState extends State { .map( (emoji) => EmojiChoiceItem( emoji: emoji, - onSelectEmoji: () => setEmoji(emoji), - // will highlight selected emoji, or the first emoji if none are selected - isDisplay: (displayEmoji == emoji || - (displayEmoji == null && emoji == emojis.first)), + onSelectEmoji: () => setEmoji(emoji, context), + isDisplay: (displayEmoji == emoji), + showShimmer: (_showShimmer && !_hasShimmered), + transformTargetId: transformTargetId(emoji), ), ) .toList(), @@ -110,12 +147,16 @@ class EmojiChoiceItem extends StatefulWidget { final String emoji; final VoidCallback onSelectEmoji; final bool isDisplay; + final bool showShimmer; + final String transformTargetId; const EmojiChoiceItem({ super.key, required this.emoji, required this.isDisplay, required this.onSelectEmoji, + required this.showShimmer, + required this.transformTargetId, }); @override @@ -125,6 +166,9 @@ class EmojiChoiceItem extends StatefulWidget { class EmojiChoiceItemState extends State { bool _isHovered = false; + LayerLink get layerLink => + MatrixState.pAnyState.layerLinkAndKey(widget.transformTargetId).link; + @override Widget build(BuildContext context) { return MouseRegion( @@ -134,25 +178,50 @@ class EmojiChoiceItemState extends State { onTap: widget.onSelectEmoji, child: Padding( padding: const EdgeInsets.all(2.0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _isHovered - ? Theme.of(context).colorScheme.primary.withAlpha(50) - : Colors.transparent, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - border: widget.isDisplay - ? Border.all( - color: AppConfig.goldLight, - width: 4, - ) - : null, - ), - child: Text( - widget.emoji, - style: Theme.of(context).textTheme.headlineSmall, - ), + child: Stack( + children: [ + CompositedTransformTarget( + link: layerLink, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _isHovered + ? Theme.of(context).colorScheme.primary.withAlpha(50) + : Colors.transparent, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + border: widget.isDisplay + ? Border.all( + color: AppConfig.goldLight, + width: 4, + ) + : null, + ), + child: Text( + widget.emoji, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ), + if (widget.showShimmer) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: Shimmer.fromColors( + baseColor: Colors.white.withValues(alpha: 0.1), + highlightColor: Colors.white.withValues(alpha: 0.6), + direction: ShimmerDirection.ltr, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.3), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + ), + ), + ), + ), + ], ), ), ), diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 4cde2206b..2b39d3a21 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -211,10 +211,13 @@ class PracticeActivityModel { } if (isCorrect) { if (activityType == ActivityTypeEnum.emoji) { - // final allEmojis = ; - choice.form.cId - .setUserLemmaInfo(UserSetLemmaInfo(emojis: [choice.choiceContent])) + .setEmojiWithXP( + emoji: choice.choiceContent, + isFromCorrectAnswer: true, + eventId: event?.eventId, + roomId: event?.room.id, + ) .then((value) { callback(); });