diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 158495497..818c7e184 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4121,7 +4121,7 @@ "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list", "practice": "Practice", "noLanguagesSet": "No languages set", - "noActivitiesFound": "No practice activities found for this message", + "noActivitiesFound": "You're all practiced for now! Come back later for more.", "hintTitle": "Hint:", "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "previous": "Previous", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index cfdc42cb8..d371276b6 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4513,6 +4513,7 @@ "autoPlayTitle": "Reproducción automática de mensajes", "autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.", "presenceStyle": "Presencia:", + "noActivitiesFound": "¡Ya has practicado por ahora! Vuelve más tarde para ver más.", "presencesToggle": "Mostrar mensajes de estado de otros usuarios", "writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}", "@writeAMessageFlag": { diff --git a/lib/main.dart b/lib/main.dart index 6be6edc91..36add47dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.local_choreo"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index 1db288973..acac979d7 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -7,7 +7,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; class PracticeActivityRecordModel { @@ -85,50 +84,17 @@ class PracticeActivityRecordModel { /// The [practiceActivity] parameter is the parent event, representing the activity itself. /// The [metadata] parameter is the metadata for the construct use, used if the record event isn't available. /// - /// The method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct and useType. - List uses( + /// The method iterates over the [responses] to get [OneConstructUse] objects for each + List usesForAllResponses( PracticeActivityModel practiceActivity, ConstructUseMetaData metadata, - ) { - try { - final List uses = []; - - final uniqueResponses = responses.toSet(); - - final List useTypes = - uniqueResponses.map((response) => response.useType).toList(); - - for (final construct in practiceActivity.tgtConstructs) { - for (final useType in useTypes) { - uses.add( - OneConstructUse( - lemma: construct.lemma, - constructType: construct.type, - useType: useType, - //TODO - find form of construct within the message - //this is related to the feature of highlighting the target construct in the message - form: construct.lemma, - metadata: metadata, - ), - ); - } - } - - return uses; - } catch (e, s) { - debugger(when: kDebugMode); - ErrorHandler.logError( - e: e, - s: s, - data: { - 'recordModel': toJson(), - 'practiceActivity': practiceActivity, - 'metadata': metadata, - }, - ); - return []; - } - } + ) => + responses + .toSet() + .expand( + (response) => response.toUses(practiceActivity, metadata), + ) + .toList(); @override bool operator ==(Object other) { @@ -164,9 +130,26 @@ class ActivityRecordResponse { required this.timestamp, }); + //TODO - differentiate into different activity types ConstructUseTypeEnum get useType => score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA; + // for each target construct create a OneConstructUse object + List toUses( + PracticeActivityModel practiceActivity, + ConstructUseMetaData metadata, + ) => + practiceActivity.tgtConstructs + .map( + (construct) => OneConstructUse( + lemma: construct.lemma, + constructType: construct.type, + useType: useType, + metadata: metadata, + ), + ) + .toList(); + factory ActivityRecordResponse.fromJson(Map json) { return ActivityRecordResponse( text: json['text'] as String?, diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 6118d5fd9..d3db75e6e 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -53,16 +53,9 @@ class MessageOverlayController extends State /// The number of activities that need to be completed before the toolbar is unlocked /// If we don't have any good activities for them, we'll decrease this number - int needed = 3; + static const int neededActivities = 3; - /// Whether the user has completed the activities needed to unlock the toolbar - /// within this overlay 'session'. if they click out and come back in then - /// we can give them some more activities to complete - int completedThisSession = 0; - - bool get finishedActivitiesThisSession => completedThisSession >= needed; - - late int activitiesLeftToComplete = needed; + int activitiesLeftToComplete = neededActivities; @override void initState() { @@ -72,8 +65,8 @@ class MessageOverlayController extends State duration: FluffyThemes.animationDuration, ); - activitiesLeftToComplete = - needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted; + activitiesLeftToComplete = activitiesLeftToComplete - + widget._pangeaMessageEvent.numberOfActivitiesCompleted; setInitialToolbarMode(); } @@ -84,7 +77,6 @@ class MessageOverlayController extends State /// and check if the toolbar should be unlocked void onActivityFinish() { if (!mounted) return; - completedThisSession += 1; activitiesLeftToComplete -= 1; clearSelection(); setState(() {}); @@ -95,8 +87,7 @@ class MessageOverlayController extends State void exitPracticeFlow() { debugPrint('Exiting practice flow'); clearSelection(); - needed = 0; - setInitialToolbarMode(); + activitiesLeftToComplete = 0; setState(() {}); } diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 16ddb164a..d3b57dc45 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -1,9 +1,14 @@ +import 'dart:developer'; + import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// The multiple choice activity view @@ -27,13 +32,9 @@ class MultipleChoiceActivityState extends State { PracticeActivityRecordModel? get currentRecordModel => widget.practiceCardController.currentCompletionRecord; - // bool get isSubmitted => - // widget.currentActivity?.latestUserRecord?.record.latestResponse != null; - @override void initState() { super.initState(); - // setCompletionRecord(); } @override @@ -42,34 +43,10 @@ class MultipleChoiceActivityState extends State { if (widget.practiceCardController.currentCompletionRecord?.responses .isEmpty ?? false) { - selectedChoiceIndex = null; + setState(() => selectedChoiceIndex = null); } } - /// Sets the completion record for the multiple choice activity. - /// If the user record is null, it creates a new record model with the question - /// from the current activity and sets the selected choice index to null. - /// Otherwise, it sets the current model to the user record's record and - /// determines the selected choice index. - void setCompletionRecord() { - // if (widget.currentActivity?.latestUserRecord?.record == null) { - widget.practiceCardController.setCompletionRecord( - PracticeActivityRecordModel( - question: - widget.currentActivity?.practiceActivity.multipleChoice!.question, - ), - ); - selectedChoiceIndex = null; - // } else { - // widget.practiceCardController.setCompletionRecord( - // widget.currentActivity!.latestUserRecord!.record); - // selectedChoiceIndex = widget - // .currentActivity?.practiceActivity.multipleChoice! - // .choiceIndex(currentRecordModel!.latestResponse!.text!); - // } - setState(() {}); - } - void updateChoice(String value, int index) { if (currentRecordModel?.hasTextResponse(value) ?? false) { return; @@ -79,22 +56,29 @@ class MultipleChoiceActivityState extends State { .currentActivity!.practiceActivity.multipleChoice! .isCorrect(value, index); - // final ConstructUseTypeEnum useType = - // isCorrect ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA; - currentRecordModel?.addResponse( text: value, score: isCorrect ? 1 : 0, ); - // TODO - add draft uses - // activities currently pass around tgtConstructs but not the token - // either we change addDraftUses to take constructs or we get and pass the token - // MatrixState.pangeaController.myAnalytics.addDraftUses( - // widget.currentActivity.practiceActivity.tg, - // widget.practiceCardController.widget.pangeaMessageEvent.room.id, - // useType, - // ); + if (currentRecordModel == null || + currentRecordModel!.latestResponse == null) { + debugger(when: kDebugMode); + return; + } + + MatrixState.pangeaController.myAnalytics.setState( + AnalyticsStream( + // note - this maybe should be the activity event id + eventId: + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + constructs: currentRecordModel!.latestResponse!.toUses( + widget.practiceCardController.currentActivity!.practiceActivity, + widget.practiceCardController.metadata, + ), + ), + ); // If the selected choice is correct, send the record and get the next activity if (widget.currentActivity!.practiceActivity.multipleChoice! diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart new file mode 100644 index 000000000..cdcacea35 --- /dev/null +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +class StarAnimationWidget extends StatefulWidget { + const StarAnimationWidget({super.key}); + + @override + _StarAnimationWidgetState createState() => _StarAnimationWidgetState(); +} + +class _StarAnimationWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacityAnimation; + late Animation _sizeAnimation; + + @override + void initState() { + super.initState(); + + // Initialize the AnimationController + _controller = AnimationController( + duration: const Duration(seconds: 1), // Duration of the animation + vsync: this, + )..repeat(reverse: true); // Repeat the animation in reverse + + // Define the opacity animation + _opacityAnimation = + Tween(begin: 0.8, end: 1.0).animate(_controller); + + // Define the size animation + _sizeAnimation = Tween(begin: 56.0, end: 60.0).animate(_controller); + } + + @override + void dispose() { + // Dispose of the controller to free resources + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + // Set constant height and width for the star container + height: 80.0, + width: 80.0, + child: Center( + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Opacity( + opacity: _opacityAnimation.value, + child: Icon( + Icons.star, + color: Colors.amber, + size: _sizeAnimation.value, + ), + ); + }, + ), + ), + ); + } +} + +class GamifiedTextWidget extends StatelessWidget { + final String userMessage; + + const GamifiedTextWidget({super.key, required this.userMessage}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children + children: [ + // Star animation above the text + const StarAnimationWidget(), + const SizedBox(height: 10), // Spacing between the star and text + Container( + constraints: const BoxConstraints( + minHeight: 80, + ), + child: Text( + userMessage, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, // Center-align the text + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 822ca78d8..0e8d4051a 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; @@ -16,6 +15,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -52,9 +52,8 @@ class MessagePracticeActivityCardState extends State { // Used to show an animation when the user completes an activity // while simultaneously fetching a new activity and not showing the loading spinner // until the appropriate time has passed to 'savor the joy' - Duration appropriateTimeForJoy = const Duration(milliseconds: 500); + Duration appropriateTimeForJoy = const Duration(milliseconds: 1000); bool savoringTheJoy = false; - Timer? joyTimer; @override void initState() { @@ -62,30 +61,28 @@ class MessagePracticeActivityCardState extends State { initialize(); } - @override - void dispose() { - joyTimer?.cancel(); - super.dispose(); - } - void _updateFetchingActivity(bool value) { if (fetchingActivity == value) return; setState(() => fetchingActivity = value); } void _setPracticeActivity(PracticeActivityEvent? activity) { + //set elsewhere but just in case + fetchingActivity = false; + + currentActivity = activity; + if (activity == null) { widget.overlayController.exitPracticeFlow(); return; } - currentActivity = activity; - + //make new completion record currentCompletionRecord = PracticeActivityRecordModel( question: activity.practiceActivity.question, ); - widget.overlayController.setSelectedSpan(currentActivity!.practiceActivity); + widget.overlayController.setSelectedSpan(activity.practiceActivity); } /// Get an existing activity if there is one. @@ -168,19 +165,26 @@ class MessagePracticeActivityCardState extends State { } } - void setCompletionRecord(PracticeActivityRecordModel? recordModel) { - currentCompletionRecord = recordModel; - } + ConstructUseMetaData get metadata => ConstructUseMetaData( + eventId: widget.pangeaMessageEvent.eventId, + roomId: widget.pangeaMessageEvent.room.id, + timeStamp: DateTime.now(), + ); - /// future that simply waits for the appropriate time to savor the joy Future _savorTheJoy() async { - joyTimer?.cancel(); - if (savoringTheJoy) return; + if (savoringTheJoy) { + //should not happen + debugger(when: kDebugMode); + } savoringTheJoy = true; - joyTimer = Timer(appropriateTimeForJoy, () { - savoringTheJoy = false; - joyTimer?.cancel(); - }); + + debugPrint('Savoring the joy'); + + await Future.delayed(appropriateTimeForJoy); + + savoringTheJoy = false; + + debugPrint('Savoring the joy is over'); } /// Called when the user finishes an activity. @@ -194,33 +198,17 @@ class MessagePracticeActivityCardState extends State { return; } - // start joy timer - _savorTheJoy(); - - final uses = currentCompletionRecord!.uses( - currentActivity!.practiceActivity, - ConstructUseMetaData( - roomId: widget.pangeaMessageEvent.room.id, - timeStamp: DateTime.now(), - ), - ); - // update the target tokens with the new construct uses + // NOTE - multiple choice activity is handling adding these to analytics await targetTokensController.updateTokensWithConstructs( - uses, + currentCompletionRecord!.usesForAllResponses( + currentActivity!.practiceActivity, + metadata, + ), context, widget.pangeaMessageEvent, ); - MatrixState.pangeaController.myAnalytics.setState( - AnalyticsStream( - // note - this maybe should be the activity event id - eventId: widget.pangeaMessageEvent.eventId, - roomId: widget.pangeaMessageEvent.room.id, - constructs: uses, - ), - ); - // save the record without awaiting to avoid blocking the UI // send a copy of the activity record to make sure its not overwritten by // the new activity @@ -240,7 +228,12 @@ class MessagePracticeActivityCardState extends State { widget.overlayController.onActivityFinish(); - _setPracticeActivity(await _fetchNewActivity()); + final Iterable result = await Future.wait([ + _savorTheJoy(), + _fetchNewActivity(), + ]); + + _setPracticeActivity(result.last as PracticeActivityEvent?); // } catch (e, s) { // debugger(when: kDebugMode); @@ -295,12 +288,7 @@ class MessagePracticeActivityCardState extends State { } String? get userMessage { - // if the user has finished all the activities to unlock the toolbar in this session - if (widget.overlayController.finishedActivitiesThisSession) { - return "Boom! Tools unlocked!"; - - // if we have no activities to show - } else if (!fetchingActivity && currentActivity == null) { + if (!fetchingActivity && currentActivity == null) { return L10n.of(context)!.noActivitiesFound; } return null; @@ -309,20 +297,7 @@ class MessagePracticeActivityCardState extends State { @override Widget build(BuildContext context) { if (userMessage != null) { - return Center( - child: Container( - constraints: const BoxConstraints( - minHeight: 80, - ), - child: Text( - userMessage!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ); + return GamifiedTextWidget(userMessage: userMessage!); } return Stack( @@ -435,7 +410,7 @@ class TargetTokensController { construct.id.type, ); if (constructUseModel != null) { - construct.xp = constructUseModel.points; + construct.xp += constructUseModel.points; construct.lastUsed = constructUseModel.lastUsed; } }