diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 33459e6c5..450d5d49a 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -127,15 +127,28 @@ class MessageAnalyticsEntry { return queue.take(3).toList(); } - /// Removes the last activity from the queue - /// This should only used when there is a startingToken in practice flow - /// and we want to go down to 2 activities + the activity with the startingToken - void goDownTo2Activities() { - if (_activityQueue.isNotEmpty && _activityQueue.length > 2) { - _activityQueue.removeLast(); + /// Adds a word focus listening activity to the front of the queue + /// And limits to 3 activities + void addForWordMeaning(PangeaToken selectedToken) { + _activityQueue.insert( + 0, + TargetTokensAndActivityType( + tokens: [selectedToken], + activityType: ActivityTypeEnum.wordMeaning, + ), + ); + // remove down to three activities + if (_activityQueue.length > 3) { + _activityQueue.removeRange(3, _activityQueue.length); } } + int get numActivities => _activityQueue.length; + + void clearActivityQueue() { + _activityQueue.clear(); + } + /// Returns a hidden word activity if there is a sequence of tokens that have hiddenWordListening in their eligibleActivityTypes TargetTokensAndActivityType? getHiddenWordActivity(int numOtherActivities) { // don't do hidden word listening on own messages diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 5b4928fdb..86b5cba38 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -65,25 +65,20 @@ class MessageOverlayController extends State List? tokens; bool initialized = false; - /// 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 - static const int neededActivities = 3; - int activitiesLeftToComplete = neededActivities; - - bool get messageInUserL2 => - pangeaMessageEvent.messageDisplayLangCode == - MatrixState.pangeaController.languageController.userL2?.langCode; - PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent; final TtsController tts = TtsController(); - bool isPlayingAudio = false; + bool _isPlayingAudio = false; bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + int get activitiesLeftToComplete => messageAnalyticsEntry?.numActivities ?? 0; + + bool get isPracticeComplete => activitiesLeftToComplete <= 0; + /// Decides whether an _initialSelectedToken should be used /// for a first practice activity on the word meaning - PangeaToken? get selectedTargetTokenForWordMeaning { + PangeaToken? get _selectedTargetTokenForWordMeaning { // if there is no initial selected token, then we don't need to do anything if (widget._initialSelectedToken == null || messageAnalyticsEntry == null) { return null; @@ -107,19 +102,30 @@ class MessageOverlayController extends State @override void initState() { super.initState(); + // debugPrint( + // "selected token: ${widget._initialSelectedToken?.text.content} total_xp:${widget._initialSelectedToken?.xp} vocab_construct_xp: ${widget._initialSelectedToken?.vocabConstruct.points} daysSincelastUseInWordMeaning ${widget._initialSelectedToken?.daysSinceLastUseByType(ActivityTypeEnum.wordMeaning)}", + // ); + // debugPrint( + // "${widget._initialSelectedToken?.vocabConstruct.uses.map((u) => "${u.useType} ${u.timeStamp}").join(", ")}", + // ); + + _getTokens(); + _setupSubscriptions(); + + if (_selectedTargetTokenForWordMeaning != null) { + messageAnalyticsEntry?.addForWordMeaning( + _selectedTargetTokenForWordMeaning!, + ); + } + } + + void _setupSubscriptions() { _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: AppConfig.overlayAnimationDuration), ); - debugPrint( - "selected token: ${widget._initialSelectedToken?.text.content} total_xp:${widget._initialSelectedToken?.xp} vocab_construct_xp: ${widget._initialSelectedToken?.vocabConstruct.points} daysSincelastUseInWordMeaning ${widget._initialSelectedToken?.daysSinceLastUseByType(ActivityTypeEnum.wordMeaning)}", - ); - debugPrint( - "${widget._initialSelectedToken?.vocabConstruct.uses.map((u) => "${u.useType} ${u.timeStamp}").join(", ")}", - ); - _reactionSubscription = widget.chatController.room.client.onSync.stream.where( (update) { @@ -141,31 +147,6 @@ class MessageOverlayController extends State ).listen((_) => setState(() {})); tts.setupTTS(); - - activitiesLeftToComplete = activitiesLeftToComplete - - widget._pangeaMessageEvent.numberOfActivitiesCompleted; - - if (pangeaMessageEvent.originalSent != null) { - pangeaMessageEvent.originalSent! - .tokensGlobal( - pangeaMessageEvent.senderId, - pangeaMessageEvent.originServerTs, - ) - .then( - (tokens) { - this.tokens = tokens; - _setInitialToolbarModeAndSelectedSpan(); - initialized = true; - }, - ).onError((e, stackTrace) { - ErrorHandler.logError(e: "Error getting tokens: $e", s: stackTrace); - _setInitialToolbarModeAndSelectedSpan(); - initialized = true; - }); - } else { - _setInitialToolbarModeAndSelectedSpan(); - initialized = true; - } } MessageAnalyticsEntry? get messageAnalyticsEntry => tokens != null @@ -177,6 +158,48 @@ class MessageOverlayController extends State ) : null; + Future _getTokens() async { + try { + final repEvent = pangeaMessageEvent.messageDisplayRepresentation; + if (repEvent != null) { + tokens = await repEvent.tokensGlobal( + pangeaMessageEvent.senderId, + pangeaMessageEvent.originServerTs, + ); + } + } catch (e, s) { + ErrorHandler.logError(e: e, s: s); + } finally { + _setInitialToolbarMode(); + initialized = true; + if (mounted) setState(() {}); + } + } + + Future _setInitialToolbarMode() async { + if (widget._pangeaMessageEvent.isAudioMessage) { + toolbarMode = MessageMode.speechToText; + return setState(() {}); + } + + // 1) we're only going to do activities if we have tokens for the message + // 2) if the user selects a span on initialization, then we want to give + // them a practice activity on that word + // 3) if the user has activities left to complete, then we want to give them + if (tokens != null && activitiesLeftToComplete > 0) { + return setState(() => toolbarMode = MessageMode.practiceActivity); + } + + // Note: this setting is now hidden so this will always be false + // leaving this here in case we want to bring it back + if (MatrixState.pangeaController.userController.profile.userSettings + .autoPlayMessages) { + return setState(() => toolbarMode = MessageMode.textToSpeech); + } + + setState(() => toolbarMode = MessageMode.translation); + } + /// We need to check if the setState call is safe to call immediately /// Kept getting the error: setState() or markNeedsBuild() called during build. /// This is a workaround to prevent that error @@ -210,61 +233,33 @@ class MessageOverlayController extends State } } - bool get isPracticeComplete => activitiesLeftToComplete <= 0; - /// When an activity is completed, we need to update the state /// and check if the toolbar should be unlocked void onActivityFinish() { if (!mounted) return; - activitiesLeftToComplete -= 1; - clearSelection(); + _clearSelection(); setState(() {}); } /// In some cases, we need to exit the practice flow and let the user /// interact with the toolbar without completing activities void exitPracticeFlow() { - clearSelection(); - activitiesLeftToComplete = 0; + messageAnalyticsEntry?.clearActivityQueue(); + _clearSelection(); setState(() {}); } - Future _setInitialToolbarModeAndSelectedSpan() async { - if (widget._pangeaMessageEvent.isAudioMessage) { - toolbarMode = MessageMode.speechToText; - return setState(() => toolbarMode = MessageMode.practiceActivity); - } - - // we're only going to do activities if we have tokens for the message - if (tokens != null) { - // if the user selects a span on initialization, then we want to give - // them a practice activity on that word - if (selectedTargetTokenForWordMeaning != null) { - _selectedSpan = selectedTargetTokenForWordMeaning?.text; - return setState(() => toolbarMode = MessageMode.practiceActivity); - } - - if (activitiesLeftToComplete > 0) { - return setState(() => toolbarMode = MessageMode.practiceActivity); - } - } - - // Note: this setting is now hidden so this will always be false - // leaving this here in case we want to bring it back - if (MatrixState.pangeaController.userController.profile.userSettings - .autoPlayMessages) { - return setState(() => toolbarMode = MessageMode.textToSpeech); - } - - setState(() => toolbarMode = MessageMode.translation); - } - - updateToolbarMode(MessageMode mode) { + void updateToolbarMode(MessageMode mode) { setState(() { toolbarMode = mode; }); } + void _clearSelection() { + _selectedSpan = null; + setState(() {}); + } + /// The text that the toolbar should target /// If there is no selectedSpan, then the whole message is the target /// If there is a selectedSpan, then the target is the selected text @@ -286,7 +281,7 @@ class MessageOverlayController extends State MessageMode.practiceActivity, // MessageMode.textToSpeech ].contains(toolbarMode) || - isPlayingAudio) { + _isPlayingAudio) { return; } @@ -306,11 +301,6 @@ class MessageOverlayController extends State setState(() {}); } - void clearSelection() { - _selectedSpan = null; - setState(() {}); - } - void setSelectedSpan(PracticeActivityModel activity) { final RelevantSpanDisplayDetails? span = activity.content.spanDisplayDetails; @@ -345,7 +335,7 @@ class MessageOverlayController extends State PangeaTokenText? get selectedSpan => _selectedSpan; - bool get hasReactions { + bool get _hasReactions { final reactionsEvents = widget._pangeaMessageEvent.event.aggregatedEvents( widget.chatController.timeline!, RelationshipTypes.reaction, @@ -353,35 +343,37 @@ class MessageOverlayController extends State return reactionsEvents.where((e) => !e.redacted).isNotEmpty; } - double get toolbarButtonsHeight => + double get _toolbarButtonsHeight => showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0; - double get reactionsHeight => hasReactions ? 28 : 0; - double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight; + double get _reactionsHeight => _hasReactions ? 28 : 0; + double get _belowMessageHeight => _toolbarButtonsHeight + _reactionsHeight; void setIsPlayingAudio(bool isPlaying) { if (mounted) { - setState(() => isPlayingAudio = isPlaying); + setState(() => _isPlayingAudio = isPlaying); } } @override void didChangeDependencies() { super.didChangeDependencies(); - if (messageSize == null || messageOffset == null || screenHeight == null) { + if (_messageSize == null || + _messageOffset == null || + _screenHeight == null) { return; } // position the overlay directly over the underlying message - final headerBottomOffset = screenHeight! - headerHeight; - final footerBottomOffset = footerHeight; - final currentBottomOffset = screenHeight! - - messageOffset!.dy - - messageSize!.height - - belowMessageHeight; + final headerBottomOffset = _screenHeight! - _headerHeight; + final footerBottomOffset = _footerHeight; + final currentBottomOffset = _screenHeight! - + _messageOffset!.dy - + _messageSize!.height - + _belowMessageHeight; final bool hasHeaderOverflow = - messageOffset!.dy < (AppConfig.toolbarMaxHeight + headerHeight); - final bool hasFooterOverflow = footerHeight > currentBottomOffset; + _messageOffset!.dy < (AppConfig.toolbarMaxHeight + _headerHeight); + final bool hasFooterOverflow = _footerHeight > currentBottomOffset; if (!hasHeaderOverflow && !hasFooterOverflow) return; @@ -392,42 +384,44 @@ class MessageOverlayController extends State // if the overlay would have a footer overflow for this message, // check if shifting the overlay up could cause a header overflow - final bottomOffsetDifference = footerHeight - currentBottomOffset; + final bottomOffsetDifference = _footerHeight - currentBottomOffset; final newTopOffset = - messageOffset!.dy - bottomOffsetDifference - belowMessageHeight; + _messageOffset!.dy - bottomOffsetDifference - _belowMessageHeight; final bool upshiftCausesHeaderOverflow = hasFooterOverflow && - newTopOffset < (headerHeight + AppConfig.toolbarMaxHeight); + newTopOffset < (_headerHeight + AppConfig.toolbarMaxHeight); if (hasHeaderOverflow || upshiftCausesHeaderOverflow) { - animationEndOffset = midpoint - messageSize!.height - belowMessageHeight; - final totalTopOffset = - animationEndOffset + messageSize!.height + AppConfig.toolbarMaxHeight; - final remainingSpace = screenHeight! - totalTopOffset; - if (remainingSpace < headerHeight) { + animationEndOffset = + midpoint - _messageSize!.height - _belowMessageHeight; + final totalTopOffset = animationEndOffset + + _messageSize!.height + + AppConfig.toolbarMaxHeight; + final remainingSpace = _screenHeight! - totalTopOffset; + if (remainingSpace < _headerHeight) { // the overlay could run over the header, so it needs to be shifted down - animationEndOffset -= (headerHeight - remainingSpace); + animationEndOffset -= (_headerHeight - remainingSpace); } scrollOffset = animationEndOffset - currentBottomOffset; } else if (hasFooterOverflow) { - scrollOffset = footerHeight - currentBottomOffset; - animationEndOffset = footerHeight; + scrollOffset = _footerHeight - currentBottomOffset; + animationEndOffset = _footerHeight; } // If, after ajusting the overlay position, the message still overflows the footer, // update the message height to fit the screen. The message is scrollable, so // this will make the both the toolbar box and the toolbar buttons visible. - if (animationEndOffset < footerHeight + belowMessageHeight) { - final double remainingSpace = screenHeight! - + if (animationEndOffset < _footerHeight + _belowMessageHeight) { + final double remainingSpace = _screenHeight! - AppConfig.toolbarMaxHeight - - headerHeight - - footerHeight - - belowMessageHeight; + _headerHeight - + _footerHeight - + _belowMessageHeight; - if (remainingSpace < messageSize!.height) { - adjustedMessageHeight = remainingSpace; + if (remainingSpace < _messageSize!.height) { + _adjustedMessageHeight = remainingSpace; } - animationEndOffset = footerHeight; + animationEndOffset = _footerHeight; } _overlayPositionAnimation = Tween( @@ -457,7 +451,7 @@ class MessageOverlayController extends State super.dispose(); } - RenderBox? get messageRenderBox { + RenderBox? get _messageRenderBox { try { return MatrixState.pAnyState.getRenderBox( widget._event.eventId, @@ -468,39 +462,39 @@ class MessageOverlayController extends State } } - Size? get messageSize { - if (messageRenderBox == null || !messageRenderBox!.hasSize) { + Size? get _messageSize { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { return null; } try { - return messageRenderBox?.size; + return _messageRenderBox?.size; } catch (e, s) { ErrorHandler.logError(e: "Error getting message size: $e", s: s); return null; } } - Offset? get messageOffset { - if (messageRenderBox == null || !messageRenderBox!.hasSize) { + Offset? get _messageOffset { + if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { return null; } try { - return messageRenderBox?.localToGlobal(Offset.zero); + return _messageRenderBox?.localToGlobal(Offset.zero); } catch (e, s) { ErrorHandler.logError(e: "Error getting message offset: $e", s: s); return null; } } - double? adjustedMessageHeight; + double? _adjustedMessageHeight; // height of the reply/forward bar + the reaction picker + contextual padding - double get footerHeight => + double get _footerHeight => 48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0); - MediaQueryData? get mediaQuery { + MediaQueryData? get _mediaQuery { try { return MediaQuery.of(context); } catch (e, s) { @@ -509,17 +503,17 @@ class MessageOverlayController extends State } } - double get headerHeight => + double get _headerHeight => (Theme.of(context).appBarTheme.toolbarHeight ?? 56) + - (mediaQuery?.padding.top ?? 0); + (_mediaQuery?.padding.top ?? 0); - double? get screenHeight => mediaQuery?.size.height; + double? get _screenHeight => _mediaQuery?.size.height; - double? get screenWidth => mediaQuery?.size.width; + double? get _screenWidth => _mediaQuery?.size.width; @override Widget build(BuildContext context) { - if (messageSize == null) return const SizedBox.shrink(); + if (_messageSize == null) return const SizedBox.shrink(); final bool showDetails = (Matrix.of(context) .store @@ -534,8 +528,8 @@ class MessageOverlayController extends State const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin; double? maxWidth; - if (screenWidth != null) { - final chatViewWidth = screenWidth! - + if (_screenWidth != null) { + final chatViewWidth = _screenWidth! - (FluffyThemes.isColumnMode(context) ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) : 0); @@ -562,7 +556,7 @@ class MessageOverlayController extends State ), const SizedBox(height: 8), SizedBox( - height: adjustedMessageHeight, + height: _adjustedMessageHeight, child: OverlayMessage( pangeaMessageEvent, immersionMode: @@ -572,15 +566,15 @@ class MessageOverlayController extends State nextEvent: widget._nextEvent, prevEvent: widget._prevEvent, timeline: widget.chatController.timeline!, - messageWidth: messageSize!.width, - messageHeight: messageSize!.height, + messageWidth: _messageSize!.width, + messageHeight: _messageSize!.height, ), ), - if (hasReactions) + if (_hasReactions) Padding( padding: const EdgeInsets.all(4), child: SizedBox( - height: reactionsHeight - 8, + height: _reactionsHeight - 8, child: MessageReactions( widget._pangeaMessageEvent.event, widget.chatController.timeline!, @@ -601,30 +595,32 @@ class MessageOverlayController extends State : 0; final double? leftPadding = - (widget._pangeaMessageEvent.ownMessage || messageOffset == null) + (widget._pangeaMessageEvent.ownMessage || _messageOffset == null) ? null - : messageOffset!.dx - horizontalPadding - columnOffset; + : _messageOffset!.dx - horizontalPadding - columnOffset; final double? rightPadding = (widget._pangeaMessageEvent.ownMessage && - screenWidth != null && - messageOffset != null && - messageSize != null) - ? screenWidth! - - messageOffset!.dx - - messageSize!.width - + _screenWidth != null && + _messageOffset != null && + _messageSize != null) + ? _screenWidth! - + _messageOffset!.dx - + _messageSize!.width - horizontalPadding : null; final positionedOverlayMessage = (_overlayPositionAnimation == null) - ? (screenHeight == null || messageSize == null || messageOffset == null) + ? (_screenHeight == null || + _messageSize == null || + _messageOffset == null) ? const SizedBox.shrink() : Positioned( left: leftPadding, right: rightPadding, - bottom: screenHeight! - - messageOffset!.dy - - messageSize!.height - - belowMessageHeight, + bottom: _screenHeight! - + _messageOffset!.dy - + _messageSize!.height - + _belowMessageHeight, child: overlayMessage, ) : AnimatedBuilder( @@ -676,25 +672,3 @@ class MessageOverlayController extends State ); } } - -class MessagePadding extends StatelessWidget { - const MessagePadding({ - super.key, - required this.child, - required this.pangeaMessageEvent, - }); - - final Widget child; - final PangeaMessageEvent pangeaMessageEvent; - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: pangeaMessageEvent.ownMessage ? 0 : Avatar.defaultSize + 16, - right: pangeaMessageEvent.ownMessage ? 8 : 0, - ), - child: child, - ); - } -} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 647dc5737..8c01880c4 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -45,6 +45,10 @@ class MessageToolbar extends StatelessWidget { ); } + if (!overLayController.initialized) { + return const ToolbarContentLoadingIndicator(); + } + // Check if the message is in the user's second language final bool messageInUserL2 = pangeaMessageEvent.messageDisplayLangCode == MatrixState.pangeaController.languageController.userL2?.langCode; @@ -145,8 +149,6 @@ class MessageToolbar extends StatelessWidget { @override Widget build(BuildContext context) { - if (!overLayController.initialized) return const SizedBox(); - return Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index e84f57566..1aee0602a 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart'; import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; @@ -10,7 +9,6 @@ import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.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'; @@ -55,9 +53,6 @@ class PracticeActivityCardState extends State { List get practiceActivities => widget.pangeaMessageEvent.practiceActivities; - // if the user has selected a token, we're going to give them an activity on that token first - late PangeaToken? startingToken; - // 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' @@ -97,7 +92,6 @@ class PracticeActivityCardState extends State { /// Get an existing activity if there is one. /// If not, get a new activity from the server. Future initialize() async { - startingToken = widget.overlayController.selectedTargetTokenForWordMeaning; _setPracticeActivity( await _fetchActivity(), ); @@ -120,26 +114,8 @@ class PracticeActivityCardState extends State { return null; } - // if the user selected a token which is not already in a hidden word activity, - // we're going to give them an activity on that token first - // otherwise, we're going to give them an activity on the next token in the queue - TargetTokensAndActivityType? nextActivitySpecs; - if (startingToken != null) { - // if the user selected a token, we're going to give them an activity on that token first - nextActivitySpecs = TargetTokensAndActivityType( - tokens: [startingToken!], - activityType: ActivityTypeEnum.wordMeaning, - ); - // clear the starting token so that the next activity is not based on it - startingToken = null; - // we want to go down to 2 activities + the activity with the startingToken - // so we remove the last activity from the queue if there's more than 2 - widget.overlayController.messageAnalyticsEntry?.goDownTo2Activities(); - } else { - nextActivitySpecs = - widget.overlayController.messageAnalyticsEntry?.nextActivity; - } - + final nextActivitySpecs = + widget.overlayController.messageAnalyticsEntry?.nextActivity; // the client is going to be choosing the next activity now // if nothing is set then it must be done with practice if (nextActivitySpecs == null) { @@ -151,7 +127,7 @@ class PracticeActivityCardState extends State { // check if we already have an activity matching the specs final existingActivity = practiceActivities.firstWhereOrNull( (activity) => - nextActivitySpecs!.matchesActivity(activity.practiceActivity), + nextActivitySpecs.matchesActivity(activity.practiceActivity), ); if (existingActivity != null) { debugPrint('found existing activity'); @@ -160,7 +136,7 @@ class PracticeActivityCardState extends State { } debugPrint( - "client requesting ${nextActivitySpecs.activityType.string} for ${nextActivitySpecs.tokens.map((t) => t.text).join(' ')}", + "client requesting ${nextActivitySpecs.activityType.string} for: ${nextActivitySpecs.tokens.map((t) => t.text.content).join(' ')}", ); final PracticeActivityModelResponse? activityResponse = @@ -168,7 +144,7 @@ class PracticeActivityCardState extends State { MessageActivityRequest( userL1: pangeaController.languageController.userL1!.langCode, userL2: pangeaController.languageController.userL2!.langCode, - messageText: widget.pangeaMessageEvent.originalSent!.text, + messageText: widget.pangeaMessageEvent.messageDisplayText, messageTokens: widget.overlayController.tokens!, activityQualityFeedback: activityFeedback, targetTokens: nextActivitySpecs.tokens,