diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b5310c8e3..fdc574009 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4215,8 +4215,9 @@ "l2SupportAlpha": "Alpha", "l2SupportBeta": "Beta", "l2SupportFull": "Full", - "voiceNotAvailable": "It looks like you don't have a voice installed for this language.", - "openVoiceSettings": "Click here to open voice settings", + "missingVoiceTitle": "Missing voice", + "voiceNotAvailable": "You don't have a voice installed for this language.", + "openVoiceSettings": "Open voice settings", "playAudio": "Play", "stop": "Stop", "grammarCopySCONJ": "Subordinating Conjunction", @@ -4337,6 +4338,24 @@ "grammarCopyCase": "Case", "grammarCopyDefinite": "Definiteness", "grammarCopyNumForm": "Numeral Form", + "grammarCopyAdn": "Adnominal", + "grammarCopyVoc": "Vocative", + "grammarCopyCmpl": "Complementizer", + "grammarCopyAdv": "Adverbial", + "grammarCopyUnknown": "Unknown", + "grammarCopyJus": "Jussive", + "grammarCopyCom": "Common", + "grammarCopyCaus": "Causative", + "grammarCopyAux": "Auxiliary", + "grammarCopyRflx": "Reflexive", + "grammarCopyPar": "Partitive", + "grammarCopySpc": "Specific", + "grammarCopyPqp": "Pluperfect", + "grammarCopyRef": "Reflexive Case", + "grammarCopyShrt": "Short", + "grammarCopyDual": "Dual", + "grammarCopyLng": "Long", + "grammarCopyMid": "Middle Voice", "grammarCopyUnknown": "Unknown", "enterPrompt": "Please enter a system prompt", "selectBotLanguage": "Select bot language", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index db5041623..203186116 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -691,6 +691,7 @@ class ChatController extends State metadata: metadata, )), ], + origin: AnalyticsUpdateOrigin.sendMessage, ), ); } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 5e82651ce..b091be925 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -56,7 +56,6 @@ class ChatEventList extends StatelessWidget { context, InstructionsEnum.clickMessage, msgEvents[0].eventId, - true, ); }); // Pangea# diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 57360d25d..340784455 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,5 +1,4 @@ import 'package:animations/animations.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -299,11 +298,12 @@ class ChatInputRow extends StatelessWidget { maxLines: 8, autofocus: !PlatformInfos.isMobile, keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter == true && - PlatformInfos.isMobile - ? TextInputAction.send - : null, // #Pangea + // textInputAction: AppConfig.sendOnEnter == true && + // PlatformInfos.isMobile + // ? TextInputAction.send + // : null, + textInputAction: TextInputAction.send, // onSubmitted: controller.onInputBarSubmitted, onSubmitted: (String value) => controller.onInputBarSubmitted(value, context), @@ -336,36 +336,36 @@ class ChatInputRow extends StatelessWidget { height: height, width: height, alignment: Alignment.center, - child: PlatformInfos.platformCanRecord && - controller.sendController.text.isEmpty - ? FloatingActionButton.small( - tooltip: L10n.of(context)!.voiceMessage, - onPressed: controller.voiceMessageAction, - elevation: 0, - heroTag: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(height), - ), - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - child: const Icon(Icons.mic_none_outlined), - ) - : + child: // #Pangea + // PlatformInfos.platformCanRecord && + // controller.sendController.text.isEmpty + // ? FloatingActionButton.small( + // tooltip: L10n.of(context)!.voiceMessage, + // onPressed: controller.voiceMessageAction, + // elevation: 0, + // heroTag: null, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(height), + // ), + // backgroundColor: theme.colorScheme.primary, + // foregroundColor: theme.colorScheme.onPrimary, + // child: const Icon(Icons.mic_none_outlined), + // ) + // : FloatingActionButton.small( + // tooltip: L10n.of(context)!.send, + // onPressed: controller.send, + // elevation: 0, + // heroTag: null, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(height), + // ), + // backgroundColor: + // theme.colorScheme.onPrimaryContainer, + // foregroundColor: theme.colorScheme.onPrimary, + // child: const Icon(Icons.send_outlined), + // ), ChoreographerSendButton(controller: controller), - // FloatingActionButton.small( - // tooltip: L10n.of(context)!.send, - // onPressed: controller.send, - // elevation: 0, - // heroTag: null, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(height), - // ), - // backgroundColor: - // theme.colorScheme.onPrimaryContainer, - // foregroundColor: theme.colorScheme.onPrimary, - // child: const Icon(Icons.send_outlined), - // ), // Pangea# ), ], diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 52d5ed351..e3df6a0e2 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart'; @@ -454,6 +455,8 @@ class ChatView extends StatelessWidget { gainColor: Theme.of(context) .colorScheme .onPrimary, + origin: AnalyticsUpdateOrigin + .sendMessage, ), const SizedBox(width: 100), ChatFloatingActionButton( diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 347d68786..818861c2e 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -8,13 +8,10 @@ import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:matrix/matrix.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:slugify/slugify.dart'; -import '../../widgets/matrix.dart'; - class InputBar extends StatelessWidget { final Room room; final int? minLines; @@ -23,7 +20,7 @@ class InputBar extends StatelessWidget { final TextInputAction? textInputAction; final ValueChanged? onSubmitted; final ValueChanged? onSubmitImage; - final FocusNode? focusNode; + final FocusNode focusNode; // #Pangea // final TextEditingController? controller; final PangeaTextController? controller; @@ -40,7 +37,7 @@ class InputBar extends StatelessWidget { this.keyboardType, this.onSubmitted, this.onSubmitImage, - this.focusNode, + required this.focusNode, this.controller, this.decoration, this.onChanged, @@ -463,100 +460,103 @@ class InputBar extends StatelessWidget { // #Pangea child: CompositedTransformTarget( link: controller!.choreographer.inputLayerLinkAndKey.link, - // Pangea# - child: TypeAheadField>( - direction: VerticalDirection.up, - hideOnEmpty: true, - hideOnLoading: true, + // child: TypeAheadField>( + // direction: VerticalDirection.up, + // hideOnEmpty: true, + // hideOnLoading: true, + // controller: controller, + // focusNode: focusNode, + // hideOnSelect: false, + // debounceDuration: const Duration(milliseconds: 50), + // // show suggestions after 50ms idle time (default is 300) + // // #Pangea + // // key: controller?.choreographer.inputLayerLinkAndKey.key, + // // builder: (context, controller, focusNode) => TextField( + // builder: (context, _, focusNode) => + child: TextField( + key: controller?.choreographer.inputLayerLinkAndKey.key, + enableSuggestions: false, + readOnly: + controller != null && controller!.choreographer.isRunningIT, + // Pangea# controller: controller, focusNode: focusNode, - hideOnSelect: false, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - // #Pangea - key: controller?.choreographer.inputLayerLinkAndKey.key, - // builder: (context, controller, focusNode) => TextField( - builder: (context, _, focusNode) => TextField( - enableSuggestions: false, - readOnly: - controller != null && controller!.choreographer.isRunningIT, - // Pangea# - controller: controller, - focusNode: focusNode, - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (KeyboardInsertedContent content) { - final data = content.data; - if (data == null) return; + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (KeyboardInsertedContent content) { + final data = content.data; + if (data == null) return; - final file = MatrixFile( - mimeType: content.mimeType, - bytes: data, - name: content.uri.split('/').last, - ); - room.sendFileEvent( - file, - shrinkImageMaxDimension: 1600, - ); - }, - ), - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - inputFormatters: [ - //#Pangea - //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), - //setting max character count to 1000 - //after max, nothing else can be typed - LengthLimitingTextInputFormatter(1000), - //Pangea# - ], - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - // #Pangea - style: controller?.exceededMaxLength ?? false - ? const TextStyle(color: Colors.red) - : null, - onTap: () { - controller?.onInputTap( - context, - fNode: focusNode, + final file = MatrixFile( + mimeType: content.mimeType, + bytes: data, + name: content.uri.split('/').last, + ); + room.sendFileEvent( + file, + shrinkImageMaxDimension: 1600, ); }, - // Pangea# - decoration: decoration!, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, ), - suggestionsCallback: getSuggestions, - itemBuilder: (c, s) => - buildSuggestion(c, s, Matrix.of(context).client), - onSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => - const SizedBox.shrink(), - loadingBuilder: (BuildContext context) => const SizedBox.shrink(), - // fix loading briefly flickering a dark box - emptyBuilder: (BuildContext context) => const SizedBox - .shrink(), // fix loading briefly showing no suggestions + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + inputFormatters: [ + //#Pangea + //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), + //setting max character count to 1000 + //after max, nothing else can be typed + LengthLimitingTextInputFormatter(1000), + //Pangea# + ], + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, // #Pangea - // If we ever want to change the suggestion background color - // here is the code for it - // decorationBuilder: (context, child) => Material( - // borderRadius: BorderRadius.circular(AppConfig.borderRadius), - // color: Theme.of(context).colorScheme.surfaceContainerHigh, - // child: child, - // ), + style: controller?.exceededMaxLength ?? false + ? const TextStyle(color: Colors.red) + : null, + onTap: () { + controller?.onInputTap( + context, + fNode: focusNode, + ); + }, // Pangea# + decoration: decoration!, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, ), + // #Pangea + // suggestionsCallback: getSuggestions, + // itemBuilder: (c, s) => + // buildSuggestion(c, s, Matrix.of(context).client), + // onSelected: (Map suggestion) => + // insertSuggestion(context, suggestion), + // errorBuilder: (BuildContext context, Object? error) => + // const SizedBox.shrink(), + // loadingBuilder: (BuildContext context) => const SizedBox.shrink(), + // // fix loading briefly flickering a dark box + // emptyBuilder: (BuildContext context) => const SizedBox + // .shrink(), // fix loading briefly showing no suggestions + + // If we ever want to change the suggestion background color + // here is the code for it + // decorationBuilder: (context, child) => Material( + // borderRadius: BorderRadius.circular(AppConfig.borderRadius), + // color: Theme.of(context).colorScheme.surfaceContainerHigh, + // child: child, + // ), + + // ), + // Pangea# ), ), ); diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 19e46b9ae..738a65126 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -1,11 +1,11 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_header.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/pangea_chat_list_header.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -78,7 +78,10 @@ class ChatListViewBody extends StatelessWidget { child: CustomScrollView( controller: controller.scrollController, slivers: [ - ChatListHeader(controller: controller), + // #Pangea + // ChatListHeader(controller: controller), + PangeaChatListHeader(controller: controller), + // Pangea# SliverList( delegate: SliverChildListDelegate( [ diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 1f7a1fbe9..58655259a 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -107,9 +107,13 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { ), ) // #Pangea - : SizedBox( + : const SizedBox( width: 0, - child: ClientChooserButton(controller), + child: ClientChooserButton( + // #Pangea + // controller + // Pangea# + ), ) // : TextButton.icon( // onPressed: controller.setServer, @@ -127,9 +131,13 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // ), // ) // Pangea# - : SizedBox( + : const SizedBox( width: 0, - child: ClientChooserButton(controller), + child: ClientChooserButton( + // #Pangea + // controller + // Pangea# + ), ), ), ), diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 0ac5ff0a8..46f5b944a 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -11,12 +11,18 @@ import 'package:go_router/go_router.dart'; // import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import 'chat_list.dart'; - class ClientChooserButton extends StatelessWidget { - final ChatListController controller; + // #Pangea + // final ChatListController controller; + // Pangea# - const ClientChooserButton(this.controller, {super.key}); + const ClientChooserButton( + // #Pangea + // this.controller, + // Pangea# + { + super.key, + }); List> _bundleMenuItems(BuildContext context) { final matrix = Matrix.of(context); @@ -268,12 +274,27 @@ class ClientChooserButton extends StatelessWidget { child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(99), - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - matrix.client.userID!.localpart, - size: 32, + child: + // #Pangea + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: + // Pangea# + Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + matrix.client.userID!.localpart, + size: 50, + ), + // #Pangea + ), + const Icon(Icons.settings_outlined, size: 20), + ], ), + // Pangea# ), ), ], @@ -296,11 +317,14 @@ class ClientChooserButton extends StatelessWidget { Object object, BuildContext context, ) async { - if (object is Client) { - controller.setActiveClient(object); - } else if (object is String) { - controller.setActiveBundle(object); - } else if (object is SettingsAction) { + // #Pangea + // if (object is Client) { + // controller.setActiveClient(object); + // } else if (object is String) { + // controller.setActiveBundle(object); + // } else + // Pangea# + if (object is SettingsAction) { switch (object) { case SettingsAction.addAccount: final consent = await showOkCancelAlertDialog( @@ -319,7 +343,10 @@ class ClientChooserButton extends StatelessWidget { // break; // Pangea# case SettingsAction.newSpace: - controller.createNewSpace(); + // #Pangea + // controller.createNewSpace(); + context.push('/rooms/newspace'); + // Pangea# break; // #Pangea // case SettingsAction.invite: diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index edb364f80..3e13549f6 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -68,7 +68,12 @@ class Choreographer { } void send(BuildContext context) { - if (!canSendMessage) return; + if (!canSendMessage) { + if (igc.igcTextData != null) { + igc.showFirstMatch(context); + } + return; + } if (pangeaController.subscriptionController.subscriptionStatus == SubscriptionStatus.showPaywall) { @@ -84,7 +89,7 @@ class Choreographer { return; } - if (!igc.hasRelevantIGCTextData) { + if (!igc.hasRelevantIGCTextData && !itController.dismissed) { getLanguageHelp().then((value) => _sendWithIGC(context)); } else { _sendWithIGC(context); @@ -201,7 +206,8 @@ class Choreographer { return; } - if (_textController.editType == EditType.igc) { + if (_textController.editType == EditType.igc || + _textController.editType == EditType.itDismissed) { _lastChecked = _textController.text; _textController.editType = EditType.keyboard; return; @@ -603,7 +609,9 @@ class Choreographer { if (isFetching) return false; // they're supposed to run IGC but haven't yet, don't let them send - if (isAutoIGCEnabled && igc.igcTextData == null) return false; + if (igc.igcTextData == null) { + return itController.dismissed; + } // if they have relevant matches, don't let them send final hasITMatches = diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index e30f30b3d..fce7c79ed 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,7 +3,9 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/enum/edit_type.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -25,6 +27,7 @@ class ITController { bool _willOpen = false; bool _isEditingSourceText = false; bool showChoiceFeedback = false; + bool dismissed = false; ITStartData? _itStartData; String? sourceText; @@ -41,6 +44,7 @@ class ITController { _willOpen = false; showChoiceFeedback = false; _isEditingSourceText = false; + dismissed = false; _itStartData = null; sourceText = null; @@ -70,9 +74,11 @@ class ITController { void closeIT() { // if the user hasn't gone through any IT steps, reset the text if (completedITSteps.isEmpty && sourceText != null) { + choreographer.textController.editType = EditType.itDismissed; choreographer.textController.text = sourceText!; } clear(); + dismissed = true; } /// if IGC isn't positive that text is full L1 then translate to L1 @@ -200,6 +206,7 @@ class ITController { final ITResponseModel res = await _customInputTranslation(currentText + nextText); + if (sourceText == null) return; nextITStep = CurrentITStep( sourceText: sourceText!, @@ -315,6 +322,7 @@ class ITController { ignoredTokens ?? [], choreographer.roomId, ConstructUseTypeEnum.ignIt, + AnalyticsUpdateOrigin.it, ); Future.delayed( diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 529100d94..76050b226 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart'; import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -80,7 +81,9 @@ class ITBarState extends State { children: [ const Positioned( top: 60, - child: PointsGainedAnimation(), + child: PointsGainedAnimation( + origin: AnalyticsUpdateOrigin.it, + ), ), SingleChildScrollView( child: Column( @@ -372,6 +375,7 @@ class ITChoices extends StatelessWidget { continuance.level > 1 ? ConstructUseTypeEnum.incIt : ConstructUseTypeEnum.corIt, + AnalyticsUpdateOrigin.it, ); } controller.currentITStep!.continuances[index].wasClicked = true; diff --git a/lib/pangea/choreographer/widgets/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart index 9fcaa927e..d6d7caa3c 100644 --- a/lib/pangea/choreographer/widgets/it_bar_buttons.dart +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -41,7 +41,6 @@ class ITBotButton extends StatelessWidget { context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, - true, ); return IconButton( @@ -51,7 +50,7 @@ class ITBotButton extends StatelessWidget { context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, - false, + showToggle: false, ), ); } diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index 6fba75395..f5e358a31 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -56,10 +56,7 @@ class ChoreographerSendButtonState extends State { color: widget.controller.choreographer.assistanceState .stateColor(context), onPressed: () { - widget.controller.choreographer.canSendMessage - ? widget.controller.choreographer.send(context) - : widget.controller.choreographer.igc - .showFirstMatch(context); + widget.controller.choreographer.send(context); }, tooltip: L10n.of(context)!.send, ), diff --git a/lib/pangea/config/environment.dart b/lib/pangea/config/environment.dart index 4d4378999..c9069e183 100644 --- a/lib/pangea/config/environment.dart +++ b/lib/pangea/config/environment.dart @@ -56,6 +56,7 @@ class Environment { return dotenv.env["RC_IOS_KEY"] ?? 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'; } + // This is a public key static String get rcStripeKey { return dotenv.env["RC_STRIPE_KEY"] ?? 'strp_YWZxWUeEfvagiefDNoofinaRCOl'; } diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 26aa7234b..1f891ccd9 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -23,8 +23,8 @@ class GetAnalyticsController { late PangeaController _pangeaController; final List _cache = []; StreamSubscription? _analyticsUpdateSubscription; - CachedStreamController> analyticsStream = - CachedStreamController>(); + CachedStreamController analyticsStream = + CachedStreamController(); /// The previous XP points of the user, before the last update. /// Used for animating analytics updates. @@ -83,7 +83,7 @@ class GetAnalyticsController { _analyticsUpdateSubscription?.cancel(); _analyticsUpdateSubscription = null; _cache.clear(); - analyticsStream.add([]); + analyticsStream.add(AnalyticsStreamUpdate(constructs: [])); prevXP = null; } @@ -92,24 +92,30 @@ class GetAnalyticsController { if (analyticsUpdate.type == AnalyticsUpdateType.server) { await getConstructs(forceUpdate: true); } - updateAnalyticsStream(); + updateAnalyticsStream(origin: analyticsUpdate.origin); } - void updateAnalyticsStream() { + void updateAnalyticsStream({AnalyticsUpdateOrigin? origin}) { // if there are no construct uses, or if the last update in this // stream has the same length as this update, don't update the stream if (allConstructUses.isEmpty || - allConstructUses.length == analyticsStream.value?.length) { + allConstructUses.length == analyticsStream.value?.constructs.length) { return; } // set the previous XP to the currentXP - if (analyticsStream.value != null && analyticsStream.value!.isNotEmpty) { - prevXP = calcXP(analyticsStream.value!); + if (analyticsStream.value != null && + analyticsStream.value!.constructs.isNotEmpty) { + prevXP = calcXP(analyticsStream.value!.constructs); } // finally, add to the stream - analyticsStream.add(allConstructUses); + analyticsStream.add( + AnalyticsStreamUpdate( + constructs: allConstructUses, + origin: origin, + ), + ); } /// Calculates the user's xpPoints for their current L2, @@ -347,3 +353,13 @@ class AnalyticsCacheEntry { return _createdAt.isBefore(lastEventUpdated); } } + +class AnalyticsStreamUpdate { + final List constructs; + final AnalyticsUpdateOrigin? origin; + + AnalyticsStreamUpdate({ + required this.constructs, + this.origin, + }); +} diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 48dc67573..9c7523991 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -125,7 +125,7 @@ class MyAnalyticsController extends BaseController { _addLocalMessage(eventID, filtered).then( (_) { _clearDraftUses(roomID); - _decideWhetherToUpdateAnalyticsRoom(level); + _decideWhetherToUpdateAnalyticsRoom(level, data.origin); }, ); }); @@ -135,6 +135,7 @@ class MyAnalyticsController extends BaseController { List tokens, String roomID, ConstructUseTypeEnum useType, + AnalyticsUpdateOrigin origin, ) { final metadata = ConstructUseMetaData( roomId: roomID, @@ -178,7 +179,7 @@ class MyAnalyticsController extends BaseController { final level = _pangeaController.analytics.level; _addLocalMessage('draft$roomID', uses).then( - (_) => _decideWhetherToUpdateAnalyticsRoom(level), + (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin), ); } @@ -218,7 +219,10 @@ class MyAnalyticsController extends BaseController { /// If the addition brought the total number of messages in the cache /// to the max, or if the addition triggered a level-up, update the analytics. /// Otherwise, add a local update to the alert stream. - void _decideWhetherToUpdateAnalyticsRoom(int prevLevel) { + void _decideWhetherToUpdateAnalyticsRoom( + int prevLevel, + AnalyticsUpdateOrigin? origin, + ) { // cancel the last timer that was set on message event and // reset it to fire after _minutesBeforeUpdate minutes _updateTimer?.cancel(); @@ -238,7 +242,7 @@ class MyAnalyticsController extends BaseController { newLevel > prevLevel ? sendLocalAnalyticsToAnalyticsRoom() : analyticsUpdateStream.add( - AnalyticsUpdate(AnalyticsUpdateType.local), + AnalyticsUpdate(AnalyticsUpdateType.local, origin: origin), ); } @@ -345,6 +349,7 @@ class MyAnalyticsController extends BaseController { class AnalyticsStream { final String eventId; final String roomId; + final AnalyticsUpdateOrigin? origin; final List constructs; @@ -352,12 +357,21 @@ class AnalyticsStream { required this.eventId, required this.roomId, required this.constructs, + this.origin, }); } +enum AnalyticsUpdateOrigin { + it, + igc, + sendMessage, + practiceActivity, +} + class AnalyticsUpdate { final AnalyticsUpdateType type; + final AnalyticsUpdateOrigin? origin; final bool isLogout; - AnalyticsUpdate(this.type, {this.isLogout = false}); + AnalyticsUpdate(this.type, {this.isLogout = false, this.origin}); } diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart index cffd64748..d9b720129 100644 --- a/lib/pangea/controllers/subscription_controller.dart +++ b/lib/pangea/controllers/subscription_controller.dart @@ -327,7 +327,10 @@ class SubscriptionController extends BaseController { SubscriptionDuration duration, { bool isPromo = false, }) async { - final Requests req = Requests(baseUrl: PApiUrls.baseAPI); + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: _pangeaController.userController.accessToken, + ); final String reqUrl = Uri.encodeFull( "${PApiUrls.paymentLink}?pangea_user_id=$userID&duration=${duration.value}&redeem=$isPromo", ); diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index 381c08c4d..53893cb7b 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -10,7 +10,6 @@ import 'package:jwt_decode/jwt_decode.dart'; import 'package:matrix/matrix.dart' as matrix; import '../models/user_model.dart'; -import '../repo/user_repo.dart'; /// Controller that manages saving and reading of user/profile information class UserController extends BaseController { @@ -124,26 +123,6 @@ class UserController extends BaseController { // wait for account data to load // as long as it's not null, then this we've already migrated the profile await _pangeaController.matrixState.client.waitForAccountData(); - if (profile.userSettings.dateOfBirth != null) { - return; - } - - // we used to store the user's profile in the pangea server - // we now store it in the matrix account data - final PangeaProfileResponse? resp = await PUserRepo.fetchPangeaUserInfo( - userID: userId!, - matrixAccessToken: _matrixAccessToken!, - ); - - // if it's null, we don't have a profile in the pangea server - if (resp?.profile == null) { - return; - } - - // if we have a profile in the pangea server, we need to migrate it to the matrix account data - final userSetting = UserSettings.fromJson(resp!.profile.toJson()); - final newProfile = Profile(userSettings: userSetting); - await newProfile.saveProfileData(waitForDataInSync: true); } /// Reinitializes the user's profile diff --git a/lib/pangea/enum/edit_type.dart b/lib/pangea/enum/edit_type.dart index 5d0a43932..775c376f8 100644 --- a/lib/pangea/enum/edit_type.dart +++ b/lib/pangea/enum/edit_type.dart @@ -5,4 +5,5 @@ enum EditType { alternativeTranslation, itGold, itStart, + itDismissed, } diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart index a42a01643..64bad0fb3 100644 --- a/lib/pangea/enum/instructions_enum.dart +++ b/lib/pangea/enum/instructions_enum.dart @@ -15,6 +15,7 @@ enum InstructionsEnum { l1Translation, translationChoices, clickAgainToDeselect, + missingVoice, } extension InstructionsEnumExtension on InstructionsEnum { @@ -28,6 +29,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return l10n.blurMeansTranslateTitle; case InstructionsEnum.tooltipInstructions: return l10n.tooltipInstructionsTitle; + case InstructionsEnum.missingVoice: + return l10n.missingVoiceTitle; case InstructionsEnum.clickAgainToDeselect: case InstructionsEnum.speechToText: case InstructionsEnum.l1Translation: @@ -64,6 +67,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return PlatformInfos.isMobile ? l10n.tooltipInstructionsMobileBody : l10n.tooltipInstructionsBrowserBody; + case InstructionsEnum.missingVoice: + return l10n.voiceNotAvailable; } } @@ -87,6 +92,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return instructionSettings.showedTranslationChoicesTooltip; case InstructionsEnum.clickAgainToDeselect: return instructionSettings.showedClickAgainToDeselect; + case InstructionsEnum.missingVoice: + return instructionSettings.showedMissingVoice; } } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 39d1bd314..74d4483dc 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -17,7 +17,6 @@ import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -83,7 +82,6 @@ class PangeaMessageEvent { Future getMatrixAudioFile( String langCode, - BuildContext context, ) async { final RepresentationEvent? rep = representationByLanguage(langCode); @@ -91,7 +89,12 @@ class PangeaMessageEvent { final TextToSpeechRequest params = TextToSpeechRequest( text: rep.content.text, - tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(), + tokens: (await rep.tokensGlobal( + senderId, + originServerTs, + )) + .map((t) => t.text) + .toList(), langCode: langCode, userL1: l1Code ?? LanguageKeys.unknownLanguage, userL2: l2Code ?? LanguageKeys.unknownLanguage, diff --git a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart index 61d298807..5d4819de9 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/token_api_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -118,16 +117,21 @@ class RepresentationEvent { return _tokens?.tokens; } - Future> tokensGlobal(BuildContext context) async { + Future> tokensGlobal( + String senderID, + DateTime timestamp, + ) async { if (tokens != null) return tokens!; - if (_event == null) { + if (_event == null && timestamp.isAfter(DateTime(2024, 9, 25))) { ErrorHandler.logError( m: 'representation with no _event and no tokens got tokens directly. This means an original_sent with no tokens. This should not happen in messages sent after September 25', s: StackTrace.current, data: { 'content': content.toJson(), 'event': _event?.toJson(), + 'timestamp': timestamp, + 'senderID': senderID, }, ); } diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index a9781f9ae..191683140 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -104,13 +104,21 @@ class OneConstructUse { : null; debugger(when: kDebugMode && constructType == null); + List categories = []; + final categoriesEntry = json['cat'] ?? json['categories']; + if (categoriesEntry != null) { + if (categoriesEntry is List) { + categories = List.from(categoriesEntry); + } else if (categoriesEntry is String) { + categories = [categoriesEntry]; + } + } + return OneConstructUse( useType: ConstructUseTypeUtil.fromString(json['useType']), lemma: json['lemma'], form: json['form'], - categories: json['categories'] != null - ? List.from(json['categories']) - : [], + categories: categories, constructType: constructType ?? ConstructTypeEnum.vocab, id: json['id'], metadata: ConstructUseMetaData( diff --git a/lib/pangea/models/practice_activities.dart/message_activity_request.dart b/lib/pangea/models/practice_activities.dart/message_activity_request.dart index 9101a78ce..edb702cda 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -195,6 +195,8 @@ class MessageActivityRequest { final String messageId; + final List clientCompatibleActivities; + MessageActivityRequest({ required this.userL1, required this.userL2, @@ -203,9 +205,28 @@ class MessageActivityRequest { required this.messageId, required this.existingActivities, required this.activityQualityFeedback, - }); + clientCompatibleActivities, + }) : clientCompatibleActivities = + clientCompatibleActivities ?? ActivityTypeEnum.values; factory MessageActivityRequest.fromJson(Map json) { + final clientCompatibleActivitiesEntry = + json['client_version_compatible_activity_types']; + List? clientCompatibleActivities; + if (clientCompatibleActivitiesEntry != null && + clientCompatibleActivitiesEntry is List) { + clientCompatibleActivities = clientCompatibleActivitiesEntry + .map( + (e) => ActivityTypeEnum.values.firstWhereOrNull( + (element) => + element.string == e as String || + element.string.split('.').last == e, + ), + ) + .where((entry) => entry != null) + .cast() + .toList(); + } return MessageActivityRequest( userL1: json['user_l1'] as String, userL2: json['user_l2'] as String, @@ -224,6 +245,10 @@ class MessageActivityRequest { json['activity_quality_feedback'] as Map, ) : null, + clientCompatibleActivities: clientCompatibleActivities != null && + clientCompatibleActivities.isNotEmpty + ? clientCompatibleActivities + : ActivityTypeEnum.values, ); } @@ -241,7 +266,7 @@ class MessageActivityRequest { // the server will only return activities of these types // this for backwards compatibility with old clients 'client_version_compatible_activity_types': - ActivityTypeEnum.values.map((e) => e.string).toList(), + clientCompatibleActivities.map((e) => e.string).toList(), }; } diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 1fdebef3a..ba55330f3 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -185,6 +185,7 @@ class UserInstructions { bool showedClickMessage; bool showedBlurMeansTranslate; bool showedTooltipInstructions; + bool showedMissingVoice; bool showedSpeechToTextTooltip; bool showedL1TranslationTooltip; @@ -200,6 +201,7 @@ class UserInstructions { this.showedL1TranslationTooltip = false, this.showedTranslationChoicesTooltip = false, this.showedClickAgainToDeselect = false, + this.showedMissingVoice = false, }); factory UserInstructions.fromJson(Map json) => @@ -219,6 +221,8 @@ class UserInstructions { json[InstructionsEnum.speechToText.toString()] ?? false, showedClickAgainToDeselect: json[InstructionsEnum.clickAgainToDeselect.toString()] ?? false, + showedMissingVoice: + json[InstructionsEnum.missingVoice.toString()] ?? false, ); Map toJson() { @@ -236,6 +240,7 @@ class UserInstructions { data[InstructionsEnum.speechToText.toString()] = showedSpeechToTextTooltip; data[InstructionsEnum.clickAgainToDeselect.toString()] = showedClickAgainToDeselect; + data[InstructionsEnum.missingVoice.toString()] = showedMissingVoice; return data; } diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 9a8421d83..6a4ba5607 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -9,58 +9,65 @@ import 'package:fluffychat/pangea/config/environment.dart'; /// /// https://api.staging.pangea.chat/api/v1/ class PApiUrls { - static String baseAPI = Environment.baseAPI; + static String choreoPrefix = "/choreo"; + static String subscriptionPrefix = "/subscription"; + static String accountPrefix = "/account"; + + static String choreoEndpoint = + "${Environment.choreoApi}${PApiUrls.choreoPrefix}"; + static String subscriptionEndpoint = + "${Environment.choreoApi}${PApiUrls.subscriptionPrefix}"; + static String accountEndpoint = + "${Environment.choreoApi}${PApiUrls.accountPrefix}"; /// ---------------------- Languages -------------------------------------- - static String getLanguages = "/languages"; + static String getLanguages = "${PApiUrls.choreoEndpoint}/languages"; /// ---------------------- Users -------------------------------------- - static String createUser = "/account/create"; - static String userDetails = "/account/get_user_access_token?pangea_user_id="; - static String updateUserProfile = "/account/update"; - static String paymentLink = "/account/payment_link"; - static String subscriptionExpiration = "/account/premium_expires_date"; + static String paymentLink = "${PApiUrls.subscriptionEndpoint}/payment_link"; /// ---------------------- Conversation Partner ------------------------- - static String searchUserProfiles = "/account/search"; + /// PTODO: Migrate or remove + static String searchUserProfiles = "${PApiUrls.accountEndpoint}/search"; ///-------------------------------- choreo -------------------------- - static String igc = "${Environment.choreoApi}/grammar"; + static String igc = "${PApiUrls.choreoEndpoint}/grammar"; static String languageDetection = - "${Environment.choreoApi}/language_detection"; + "${PApiUrls.choreoEndpoint}/language_detection"; - static String igcLite = "${Environment.choreoApi}/grammar_lite"; - static String spanDetails = "${Environment.choreoApi}/span_details"; + static String igcLite = "${PApiUrls.choreoEndpoint}/grammar_lite"; + static String spanDetails = "${PApiUrls.choreoEndpoint}/span_details"; - static String wordNet = "${Environment.choreoApi}/wordnet"; + static String wordNet = "${PApiUrls.choreoEndpoint}/wordnet"; static String contextualizedTranslation = - "${Environment.choreoApi}/translation/contextual"; + "${PApiUrls.choreoEndpoint}/translation/contextual"; static String simpleTranslation = - "${Environment.choreoApi}/translation/direct"; - static String tokenize = "${Environment.choreoApi}/tokenize"; + "${PApiUrls.choreoEndpoint}/translation/direct"; + static String tokenize = "${PApiUrls.choreoEndpoint}/tokenize"; static String contextualDefinition = - "${Environment.choreoApi}/contextual_definition"; - static String similarity = "${Environment.choreoApi}/similarity"; - static String topicInfo = "${Environment.choreoApi}/vocab_list"; + "${PApiUrls.choreoEndpoint}/contextual_definition"; + static String similarity = "${PApiUrls.choreoEndpoint}/similarity"; + static String topicInfo = "${PApiUrls.choreoEndpoint}/vocab_list"; - static String itFeedback = "${Environment.choreoApi}/translation/feedback"; + static String itFeedback = "${PApiUrls.choreoEndpoint}/translation/feedback"; - static String firstStep = "/it_initialstep"; - static String subseqStep = "/it_step"; + static String firstStep = "${PApiUrls.choreoEndpoint}/it_initialstep"; + static String subseqStep = "${PApiUrls.choreoEndpoint}/it_step"; - static String textToSpeech = "${Environment.choreoApi}/text_to_speech"; - static String speechToText = "${Environment.choreoApi}/speech_to_text"; + static String textToSpeech = "${PApiUrls.choreoEndpoint}/text_to_speech"; + static String speechToText = "${PApiUrls.choreoEndpoint}/speech_to_text"; static String messageActivityGeneration = - "${Environment.choreoApi}/practice/message"; + "${PApiUrls.choreoEndpoint}/practice/message"; ///-------------------------------- revenue cat -------------------------- - static String rcApiV1 = "https://api.revenuecat.com/v1"; - static String rcApiV2 = - "https://api.revenuecat.com/v2/projects/${Environment.rcProjectId}"; - static String rcApps = "$rcApiV2/apps"; - static String rcProducts = "$rcApiV2/offerings?expand=items.package.product"; - static String rcSubscribers = "$rcApiV1/subscribers"; + static String rcApiV1 = "https://api.revenuecat.com/v1"; + + static String rcAppsChoreo = "${PApiUrls.subscriptionEndpoint}/app_ids"; + static String rcProductsChoreo = + "${PApiUrls.subscriptionEndpoint}/all_products"; + + static String rcSubscription = "$rcApiV1/subscribers"; } diff --git a/lib/pangea/repo/image_repo.dart b/lib/pangea/repo/image_repo.dart index 624e06e22..d12f02975 100644 --- a/lib/pangea/repo/image_repo.dart +++ b/lib/pangea/repo/image_repo.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:developer'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; @@ -44,9 +45,12 @@ class GenerateImageRequest { class ImageRepo { static Future fetchImage( - GenerateImageRequest request) async { - final Requests req = - Requests(baseUrl: Environment.choreoApi); // Set your API base URL + GenerateImageRequest request, + ) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); // Set your API base URL final requestBody = request.toJson(); try { @@ -58,7 +62,8 @@ class ImageRepo { if (res.statusCode == 200) { final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); return GenerateImageeResponse.fromJson( - decodedBody); // Convert response to ImageModel + decodedBody, + ); // Convert response to ImageModel } else { throw Exception('Failed to load image'); } diff --git a/lib/pangea/repo/interactive_translation_repo.dart b/lib/pangea/repo/interactive_translation_repo.dart index ab5d3b1d6..6e1739ecf 100644 --- a/lib/pangea/repo/interactive_translation_repo.dart +++ b/lib/pangea/repo/interactive_translation_repo.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:http/http.dart'; -import 'package:fluffychat/pangea/config/environment.dart'; import '../models/custom_input_translation_model.dart'; import '../models/it_response_model.dart'; import '../models/system_choice_translation_model.dart'; @@ -14,8 +15,8 @@ class ITRepo { CustomInputRequestModel initalText, ) async { final Requests req = Requests( - baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, ); final Response res = await req.post(url: PApiUrls.firstStep, body: initalText.toJson()); @@ -29,8 +30,8 @@ class ITRepo { SystemChoiceRequestModel subseqText, ) async { final Requests req = Requests( - baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, ); final Response res = diff --git a/lib/pangea/repo/language_repo.dart b/lib/pangea/repo/language_repo.dart index 9ca12b25e..4c0589750 100644 --- a/lib/pangea/repo/language_repo.dart +++ b/lib/pangea/repo/language_repo.dart @@ -1,18 +1,20 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import '../config/environment.dart'; import '../network/requests.dart'; class LanguageRepo { static Future> fetchLanguages() async { - final Requests req = Requests(baseUrl: Environment.choreoApi); + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + ); final Response res = await req.get(url: PApiUrls.getLanguages); final decodedBody = diff --git a/lib/pangea/repo/subscription_repo.dart b/lib/pangea/repo/subscription_repo.dart index 293977921..d50cd1c6f 100644 --- a/lib/pangea/repo/subscription_repo.dart +++ b/lib/pangea/repo/subscription_repo.dart @@ -3,28 +3,30 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/network/requests.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../network/urls.dart'; class SubscriptionRepo { - static final Map requestHeaders = { - 'Content-type': 'application/json', - 'Accept': 'application/json', - 'Authorization': 'Bearer ${Environment.rcKey}', - }; - static Future getAppIds() async { try { - final http.Response res = await http.get( - Uri.parse(PApiUrls.rcApps), - headers: SubscriptionRepo.requestHeaders, + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + final http.Response res = await req.get( + url: PApiUrls.rcAppsChoreo, + ); + + return SubscriptionAppIds.fromJson( + jsonDecode(res.body), ); - final json = jsonDecode(res.body); - return SubscriptionAppIds.fromJson(json); } catch (err) { ErrorHandler.logError( m: "Failed to fetch app information for revenuecat API", @@ -36,19 +38,19 @@ class SubscriptionRepo { static Future?> getAllProducts() async { try { - final http.Response res = await http.get( - Uri.parse(PApiUrls.rcProducts), - headers: SubscriptionRepo.requestHeaders, + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + final http.Response res = await req.get( + url: PApiUrls.rcProductsChoreo, ); final Map json = jsonDecode(res.body); final RCProductsResponseModel resp = RCProductsResponseModel.fromJson(json); return resp.allProducts; - } catch (err) { - ErrorHandler.logError( - m: "Failed to fetch entitlement information for revenuecat API", - s: StackTrace.current, - ); + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); return null; } } @@ -62,7 +64,7 @@ class SubscriptionRepo { 'Accept': 'application/json', 'Authorization': 'Bearer ${Environment.rcStripeKey}', }; - final String url = "${PApiUrls.rcSubscribers}/$userId"; + final String url = "${PApiUrls.rcSubscription}/$userId"; final http.Response res = await http.get( Uri.parse(url), headers: stripeHeaders, @@ -88,26 +90,20 @@ class RCProductsResponseModel { Map json, ) { final List offerings = json["items"] as List; - final offering = offerings.firstWhereOrNull( - Environment.isStaging - ? (offering) => !(offering['is_current'] as bool) - : (offering) => offering['is_current'] as bool, - ); - final Map metadata = offering['metadata']; - - final List allProducts = []; - for (final packageDetails in offering['packages']['items']) { - final String packageId = packageDetails['id']; - final List products = - RCProductsResponseModel.productsFromPackageDetails( - packageDetails, - packageId, - metadata, - ); - allProducts.addAll(products); - } - - return RCProductsResponseModel(allProducts: allProducts); + final res = offerings + .map( + (offering) => SubscriptionDetails( + price: offering['price'], + duration: SubscriptionDuration.values.firstWhereOrNull( + (duration) => duration.value == offering['duration'], + ), + id: offering['id'], + appId: offering['appId'], + ), + ) + .toList() + .cast(); + return RCProductsResponseModel(allProducts: res); } static List productsFromPackageDetails( diff --git a/lib/pangea/repo/user_repo.dart b/lib/pangea/repo/user_repo.dart index 7f3efc48f..ec6f29c01 100644 --- a/lib/pangea/repo/user_repo.dart +++ b/lib/pangea/repo/user_repo.dart @@ -1,66 +1,14 @@ import 'dart:convert'; -import 'dart:developer'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:http/http.dart'; -import '../models/user_model.dart'; import '../models/user_profile_search_model.dart'; import '../network/requests.dart'; import '../network/urls.dart'; class PUserRepo { - // static Future repoCreatePangeaUser({ - // required String userID, - // required String dob, - // required fullName, - // required String matrixAccessToken, - // }) async { - // try { - // final Requests req = Requests( - // baseUrl: PApiUrls.baseAPI, - // matrixAccessToken: matrixAccessToken, - // ); - - // final Map body = { - // ModelKey.userFullName: fullName, - // ModelKey.userPangeaUserId: userID, - // ModelKey.userDateOfBirth: dob, - // }; - // final resp = await req.post( - // url: PApiUrls.createUser, - // body: body, - // ); - // return PangeaProfileResponse.fromJson(jsonDecode(resp.body)); - // } catch (err, s) { - // ErrorHandler.logError(e: err, s: s); - // return null; - // } - // } - - static Future fetchPangeaUserInfo({ - required String userID, - required String matrixAccessToken, - }) async { - Response res; - try { - final Requests req = Requests( - baseUrl: PApiUrls.baseAPI, - matrixAccessToken: matrixAccessToken, - ); - res = await req.get( - url: PApiUrls.userDetails, - objectId: userID, - ); - - return PangeaProfileResponse.fromJson(jsonDecode(res.body)); - } catch (err) { - //status code should be 400 - PTODO - check ffor this. - log("Most likely a first signup and needs to make an account"); - return null; - } - } - static Future searchUserProfiles({ // List? interests, String? targetLanguage, @@ -72,8 +20,8 @@ class PUserRepo { required int limit, }) async { final Requests req = Requests( - baseUrl: PApiUrls.baseAPI, accessToken: accessToken, + choreoApiKey: Environment.choreoApiKey, ); final Map body = {}; // if (interests != null) body[ModelKey.userInterests] = interests.toString(); diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 681c0de08..a4e8ef151 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -56,6 +56,9 @@ class InstructionsController { case InstructionsEnum.clickAgainToDeselect: profile.instructionSettings.showedClickAgainToDeselect = value; break; + case InstructionsEnum.missingVoice: + profile.instructionSettings.showedMissingVoice = value; + break; } return profile; }); @@ -66,9 +69,10 @@ class InstructionsController { Future showInstructionsPopup( BuildContext context, InstructionsEnum key, - String transformTargetKey, [ + String transformTargetKey, { bool showToggle = true, - ]) async { + Widget? customContent, + }) async { final bool userLangsSet = await _pangeaController.userController.areUserLanguagesSet; if (!userLangsSet) { @@ -115,6 +119,7 @@ class InstructionsController { style: botStyle, ), ), + if (customContent != null) customContent, if (showToggle) InstructionsToggle(instructionsKey: key), ], ), diff --git a/lib/pangea/utils/subscription_app_id.dart b/lib/pangea/utils/subscription_app_id.dart index c6de8867c..de2332ed3 100644 --- a/lib/pangea/utils/subscription_app_id.dart +++ b/lib/pangea/utils/subscription_app_id.dart @@ -22,24 +22,11 @@ class SubscriptionAppIds { return null; } - factory SubscriptionAppIds.fromJson(json) { - final SubscriptionAppIds appIds = SubscriptionAppIds(); - for (final appInfo in (json['items'] as List)) { - final String platform = appInfo['type']; - final String appId = appInfo['id']; - switch (platform) { - case 'stripe': - appIds.stripeId = appId; - continue; - case 'app_store': - appIds.appleId = appId; - continue; - case 'play_store': - appIds.androidId = appId; - continue; - } - } - return appIds; + factory SubscriptionAppIds.fromJson(Map json) { + return SubscriptionAppIds() + ..stripeId = json['stripe_id'] + ..androidId = json['android_id'] + ..appleId = json['apple_id']; } } diff --git a/lib/pangea/widgets/animations/gain_points.dart b/lib/pangea/widgets/animations/gain_points.dart index 13ae324e7..d9d9a0111 100644 --- a/lib/pangea/widgets/animations/gain_points.dart +++ b/lib/pangea/widgets/animations/gain_points.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -8,8 +9,11 @@ import 'package:flutter/material.dart'; class PointsGainedAnimation extends StatefulWidget { final Color? gainColor; final Color? loseColor; + final AnalyticsUpdateOrigin origin; + const PointsGainedAnimation({ super.key, + required this.origin, this.gainColor, this.loseColor = Colors.red, }); @@ -69,7 +73,8 @@ class PointsGainedAnimationState extends State super.dispose(); } - void _showPointsGained(List constructs) { + void _showPointsGained(AnalyticsStreamUpdate update) { + if (update.origin != widget.origin) return; setState(() => _addedPoints = (_currentXP ?? 0) - (_prevXP ?? 0)); if (_prevXP != _currentXP) { _controller.reset(); diff --git a/lib/pangea/widgets/animations/progress_bar/level_bar.dart b/lib/pangea/widgets/animations/progress_bar/level_bar.dart index fb8461f43..78b4d4944 100644 --- a/lib/pangea/widgets/animations/progress_bar/level_bar.dart +++ b/lib/pangea/widgets/animations/progress_bar/level_bar.dart @@ -19,12 +19,14 @@ class LevelBar extends StatefulWidget { class LevelBarState extends State { double prevWidth = 0; + double get width => + widget.progressBarDetails.totalWidth * widget.details.widthMultiplier; @override void didUpdateWidget(covariant LevelBar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.details.currentPoints != widget.details.currentPoints) { - setState(() => prevWidth = widget.details.width); + setState(() => prevWidth = width); } } @@ -33,7 +35,7 @@ class LevelBarState extends State { return AnimatedLevelBar( height: widget.progressBarDetails.height, beginWidth: prevWidth, - endWidth: widget.details.width, + endWidth: width, decoration: BoxDecoration( borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), diff --git a/lib/pangea/widgets/animations/progress_bar/progress_bar.dart b/lib/pangea/widgets/animations/progress_bar/progress_bar.dart index ea0263a3c..a8383bae5 100644 --- a/lib/pangea/widgets/animations/progress_bar/progress_bar.dart +++ b/lib/pangea/widgets/animations/progress_bar/progress_bar.dart @@ -6,31 +6,55 @@ import 'package:flutter/material.dart'; // Provide an order list of level indicators, each with it's color // and stream. Also provide an overall width and pointsPerLevel. -class ProgressBar extends StatelessWidget { +class ProgressBar extends StatefulWidget { final List levelBars; - final ProgressBarDetails progressBarDetails; const ProgressBar({ super.key, required this.levelBars, - required this.progressBarDetails, }); + @override + ProgressBarState createState() => ProgressBarState(); +} + +class ProgressBarState extends State { + double width = 0; + void setWidth(double newWidth) { + if (width != newWidth) { + setState(() => width = newWidth); + } + } + + get progressBarDetails => ProgressBarDetails( + totalWidth: width, + borderColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + ); + @override Widget build(BuildContext context) { - return Stack( - alignment: Alignment.centerLeft, - children: [ - ProgressBarBackground(details: progressBarDetails), - for (final levelBar in levelBars) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2), - child: LevelBar( - details: levelBar, - progressBarDetails: progressBarDetails, - ), - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + if (width != constraints.maxWidth) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setWidth(constraints.maxWidth), + ); + } + return Stack( + alignment: Alignment.centerLeft, + children: [ + ProgressBarBackground(details: progressBarDetails), + for (final levelBar in widget.levelBars) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: LevelBar( + details: levelBar, + progressBarDetails: progressBarDetails, + ), + ), + ], + ); + }, ); } } diff --git a/lib/pangea/widgets/animations/progress_bar/progress_bar_details.dart b/lib/pangea/widgets/animations/progress_bar/progress_bar_details.dart index 9ff4df142..347a98a49 100644 --- a/lib/pangea/widgets/animations/progress_bar/progress_bar_details.dart +++ b/lib/pangea/widgets/animations/progress_bar/progress_bar_details.dart @@ -3,12 +3,12 @@ import 'dart:ui'; class LevelBarDetails { final Color fillColor; final int currentPoints; - final double width; + final double widthMultiplier; const LevelBarDetails({ required this.fillColor, required this.currentPoints, - required this.width, + required this.widthMultiplier, }); } diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 47cb41af8..cc41605c9 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -71,7 +71,11 @@ class MessageAudioCardState extends State { final PangeaTokenText selection = widget.selection!; final tokenText = selection.content; - await widget.tts.speak(tokenText); + await widget.tts.tryToSpeak( + tokenText, + context, + widget.messageEvent.eventId, + ); } void setSectionStartAndEnd(int? start, int? end) => mounted @@ -158,7 +162,6 @@ class MessageAudioCardState extends State { } else { audioFile = await widget.messageEvent.getMatrixAudioFile( langCode, - context, ); } debugPrint("audio file is now: $audioFile. setting starts and ends..."); @@ -197,19 +200,13 @@ class MessageAudioCardState extends State { child: _isLoading ? const ToolbarContentLoadingIndicator() : audioFile != null - ? Column( - children: [ - AudioPlayerWidget( - null, - matrixFile: audioFile, - sectionStartMS: sectionStartMS, - sectionEndMS: sectionEndMS, - color: - Theme.of(context).colorScheme.onPrimaryContainer, - setIsPlayingAudio: widget.setIsPlayingAudio, - ), - widget.tts.missingVoiceButton, - ], + ? AudioPlayerWidget( + null, + matrixFile: audioFile, + sectionStartMS: sectionStartMS, + sectionEndMS: sectionEndMS, + color: Theme.of(context).colorScheme.onPrimaryContainer, + setIsPlayingAudio: widget.setIsPlayingAudio, ) : const CardErrorWidget( error: "Null audio file in message_audio_card", diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index bc6ed54bc..ffca4f32c 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -47,14 +47,6 @@ class MessageToolbar extends StatelessWidget { final bool messageInUserL2 = pangeaMessageEvent.messageDisplayLangCode == MatrixState.pangeaController.languageController.userL2?.langCode; - // If not in the target language show specific messsage - if (!messageInUserL2) { - return MessageDisplayCard( - displayText: - L10n.of(context)!.messageNotInTargetLang, // Pass the display text, - ); - } - switch (overLayController.toolbarMode) { case MessageMode.translation: return MessageTranslationCard( @@ -104,6 +96,13 @@ class MessageToolbar extends StatelessWidget { } } case MessageMode.practiceActivity: + // If not in the target language show specific messsage + if (!messageInUserL2) { + return MessageDisplayCard( + displayText: L10n.of(context)! + .messageNotInTargetLang, // Pass the display text, + ); + } return PracticeActivityCard( pangeaMessageEvent: pangeaMessageEvent, overlayController: overLayController, diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 190a0fdff..7cead7b5a 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; class ToolbarButtons extends StatelessWidget { @@ -25,10 +26,16 @@ class ToolbarButtons extends StatelessWidget { .where((mode) => mode.shouldShowAsToolbarButton(pangeaMessageEvent.event)) .toList(); + bool get messageInUserL2 => + pangeaMessageEvent.messageDisplayLangCode == + MatrixState.pangeaController.languageController.userL2?.langCode; + static const double iconWidth = 36.0; @override Widget build(BuildContext context) { + final totallyDone = + overlayController.isPracticeComplete || !messageInUserL2; final double barWidth = width - iconWidth; if (overlayController.pangeaMessageEvent.isAudioMessage) { @@ -85,14 +92,14 @@ class ToolbarButtons extends StatelessWidget { index, overlayController.toolbarMode, pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, + totallyDone, ), ), ), onPressed: mode.isUnlocked( index, pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, + totallyDone, ) ? () => overlayController.updateToolbarMode(mode) : null, diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart index 1765a9d20..67c94494b 100644 --- a/lib/pangea/widgets/chat/missing_voice_button.dart +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -2,26 +2,23 @@ import 'dart:io'; import 'package:android_intent_plus/android_intent.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; class MissingVoiceButton extends StatelessWidget { - final String targetLangCode; + const MissingVoiceButton({super.key}); - const MissingVoiceButton({ - required this.targetLangCode, - super.key, - }); - - void launchTTSSettings(BuildContext context) { - if (Platform.isAndroid) { + Future launchTTSSettings(BuildContext context) async { + if (!kIsWeb && Platform.isAndroid) { const intent = AndroidIntent( action: 'com.android.settings.TTS_SETTINGS', package: 'com.talktolearn.chat', ); - showFutureLoadingDialog( + await showFutureLoadingDialog( context: context, future: intent.launch, ); @@ -30,37 +27,18 @@ class MissingVoiceButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1), - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), + return TextButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + AppConfig.primaryColor.withOpacity(0.1), ), ), - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(top: 8), - child: SizedBox( - width: AppConfig.toolbarMinWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - L10n.of(context)!.voiceNotAvailable, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: () => launchTTSSettings, - // commenting out as suspecting this is causing an issue - // #freeze-activity - style: const ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text(L10n.of(context)!.openVoiceSettings), - ), - ], - ), + onPressed: () async { + MatrixState.pAnyState.closeOverlay(); + await launchTTSSettings(context); + }, + child: Center( + child: Text(L10n.of(context)!.openVoiceSettings), ), ); } diff --git a/lib/pangea/widgets/chat/overlay_message_text.dart b/lib/pangea/widgets/chat/overlay_message_text.dart index 1ca2f8473..f23cbba5c 100644 --- a/lib/pangea/widgets/chat/overlay_message_text.dart +++ b/lib/pangea/widgets/chat/overlay_message_text.dart @@ -32,7 +32,10 @@ class OverlayMessageTextState extends State { tokens = widget.pangeaMessageEvent.originalSent?.tokens; if (widget.pangeaMessageEvent.originalSent != null && tokens == null) { widget.pangeaMessageEvent.originalSent! - .tokensGlobal(context) + .tokensGlobal( + widget.pangeaMessageEvent.senderId, + widget.pangeaMessageEvent.originServerTs, + ) .then((tokens) { // this isn't currently working because originalSent's _event is null setState(() => this.tokens = tokens); @@ -142,7 +145,7 @@ class OverlayMessageTextState extends State { widget.overlayController.onClickOverlayMessageToken( tokenPosition.token!, ); - setState(() {}); + if (mounted) setState(() {}); }, text: substring, style: style.merge( diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index c98cb6220..4c178d440 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -1,8 +1,8 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -45,8 +45,8 @@ class TtsController { await tts.awaitSpeakCompletion(true); - final voices = await tts.getVoices; - availableLangCodes = (voices as List) + final voices = (await tts.getVoices) as List?; + availableLangCodes = (voices ?? []) .map((v) { // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; @@ -85,6 +85,37 @@ class TtsController { } } + Future showMissingVoicePopup( + BuildContext context, + String eventID, + ) async { + await MatrixState.pangeaController.instructions.showInstructionsPopup( + context, + InstructionsEnum.missingVoice, + eventID, + showToggle: false, + customContent: const Padding( + padding: EdgeInsets.only(top: 12), + child: MissingVoiceButton(), + ), + ); + return; + } + + /// A safer version of speak, that handles the case of + /// the language not being supported by the TTS engine + Future tryToSpeak( + String text, + BuildContext context, + String eventID, + ) async { + if (isLanguageFullySupported) { + await speak(text); + } else { + await showMissingVoicePopup(context, eventID); + } + } + Future speak(String text) async { try { stop(); @@ -112,11 +143,4 @@ class TtsController { bool get isLanguageFullySupported => availableLangCodes.contains(targetLanguage); - - Widget get missingVoiceButton => targetLanguage != null && - (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) - ? const SizedBox.shrink() - : MissingVoiceButton( - targetLangCode: targetLanguage!, - ); } diff --git a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart index 0a616c6bd..980ee1488 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart @@ -1,21 +1,21 @@ import 'dart:async'; -import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; +import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar.dart'; import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart'; import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart'; import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart'; -import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/pangea/widgets/flag.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; /// A summary of "My Analytics" shown at the top of the chat list /// It shows a variety of progress indicators such as @@ -37,7 +37,7 @@ class LearningProgressIndicatorsState /// A stream subscription to listen for updates to /// the analytics data, either locally or from events - StreamSubscription>? _analyticsUpdateSubscription; + StreamSubscription? _analyticsUpdateSubscription; /// Vocabulary constructs model ConstructListModel? words; @@ -65,11 +65,11 @@ class LearningProgressIndicatorsState void initState() { super.initState(); updateAnalyticsData( - _pangeaController.analytics.analyticsStream.value ?? [], + _pangeaController.analytics.analyticsStream.value?.constructs ?? [], ); _analyticsUpdateSubscription = _pangeaController .analytics.analyticsStream.stream - .listen(updateAnalyticsData); + .listen((update) => updateAnalyticsData(update.constructs)); } @override @@ -119,7 +119,7 @@ class LearningProgressIndicatorsState } } - double get levelBarWidth => FluffyThemes.columnWidth - (32 * 2) - 25; + // double get levelBarWidth => FluffyThemes.columnWidth - (32 * 2) - 25; Color levelColor(int level) { final colors = [ @@ -146,19 +146,14 @@ class LearningProgressIndicatorsState ? const Color.fromARGB(255, 0, 190, 83) : Theme.of(context).colorScheme.primary, currentPoints: currentXP, - width: levelBarWidth * _pangeaController.analytics.levelProgress, + widthMultiplier: _pangeaController.analytics.levelProgress, ), LevelBarDetails( fillColor: Theme.of(context).colorScheme.primary, currentPoints: serverXP, - width: - levelBarWidth * _pangeaController.analytics.serverLevelProgress, + widthMultiplier: _pangeaController.analytics.serverLevelProgress, ), ], - progressBarDetails: ProgressBarDetails( - totalWidth: levelBarWidth, - borderColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), ); final levelBadge = Container( @@ -183,84 +178,67 @@ class LearningProgressIndicatorsState ), ); - return Stack( - alignment: Alignment.topCenter, + return Row( children: [ - // const Positioned( - // child: PointsGainedAnimation(), - // ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(46, 0, 32, 4), - child: Row( + const ClientChooserButton(), + const SizedBox(width: 6), + Expanded( + child: Column( + children: [ + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - FutureBuilder( - future: _pangeaController.matrixState.client - .getProfileFromUserId( - _pangeaController.matrixState.client.userID!, - ), - builder: (context, snapshot) { - final mxid = Matrix.of(context).client.userID ?? - L10n.of(context)!.user; - return Avatar( - name: snapshot.data?.displayName ?? - mxid.localpart ?? - mxid, - mxContent: snapshot.data?.avatarUrl, - size: 40, - ); - }, - ), Row( - mainAxisAlignment: MainAxisAlignment.center, children: ProgressIndicatorEnum.values - .where( - (indicator) => - indicator != ProgressIndicatorEnum.level, - ) + .where((i) => i != ProgressIndicatorEnum.level) .map( - (indicator) => ProgressIndicatorBadge( - points: getProgressPoints(indicator), - onTap: () { - final model = getConstructsModel(indicator); - if (model == null) return; - showDialog( - context: context, - builder: (c) => AnalyticsPopup( - indicator: indicator, - constructsModel: model, - ), - ); - }, - progressIndicator: indicator, - loading: loading, + (indicator) => Padding( + padding: const EdgeInsets.only(right: 10), + child: ProgressIndicatorBadge( + points: getProgressPoints(indicator), + onTap: () { + final model = getConstructsModel(indicator); + if (model == null) return; + showDialog( + context: context, + builder: (c) => AnalyticsPopup( + indicator: indicator, + constructsModel: model, + ), + ); + }, + progressIndicator: indicator, + loading: loading, + ), ), ) .toList(), ), + InkWell( + onTap: () => showDialog( + context: context, + builder: (c) => const SettingsLearning(), + ), + child: LanguageFlag( + language: _pangeaController.languageController.userL2, + size: 24, + ), + ), ], ), - ), - Center( - child: SizedBox( + const SizedBox(height: 6), + SizedBox( height: 36, - child: SizedBox( - width: levelBarWidth + 16, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned(left: 16, right: 0, child: progressBar), - Positioned(left: 0, child: levelBadge), - ], - ), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned(left: 16, right: 0, child: progressBar), + Positioned(left: 0, child: levelBadge), + ], ), ), - ), - const SizedBox(height: 16), - ], + ], + ), ), ], ); diff --git a/lib/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart b/lib/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart index bd24b206f..f05ac634c 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart @@ -18,40 +18,34 @@ class ProgressIndicatorBadge extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), - child: Tooltip( - message: progressIndicator.tooltip(context), - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - progressIndicator.icon, - color: progressIndicator.color(context), - ), - const SizedBox(width: 5), - !loading - ? Text( - points?.toString() ?? '0', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ) - : const SizedBox( - height: 8, - width: 8, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ), - ], + return Tooltip( + message: progressIndicator.tooltip(context), + child: InkWell( + onTap: onTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + progressIndicator.icon, + color: progressIndicator.color(context), ), - ), + const SizedBox(width: 5), + !loading + ? Text( + points?.toString() ?? '0', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ) + : const SizedBox( + height: 8, + width: 8, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ], ), ), ); diff --git a/lib/pangea/widgets/chat_list/pangea_chat_list_header.dart b/lib/pangea/widgets/chat_list/pangea_chat_list_header.dart new file mode 100644 index 000000000..5fdec6839 --- /dev/null +++ b/lib/pangea/widgets/chat_list/pangea_chat_list_header.dart @@ -0,0 +1,82 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class PangeaChatListHeader extends StatelessWidget + implements PreferredSizeWidget { + final ChatListController controller; + final bool globalSearch; + + const PangeaChatListHeader({ + super.key, + required this.controller, + this.globalSearch = true, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SliverList( + delegate: SliverChildListDelegate( + [ + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + ), + child: Column( + children: [ + const LearningProgressIndicators(), + const SizedBox(height: 16), + TextField( + controller: controller.searchController, + focusNode: controller.searchFocusNode, + textInputAction: TextInputAction.search, + onChanged: (text) => controller.onSearchEnter( + text, + globalSearch: globalSearch, + ), + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context)!.searchChatsRooms, + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: controller.isSearchMode + ? IconButton( + tooltip: L10n.of(context)!.cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelSearch, + color: theme.colorScheme.onPrimaryContainer, + ) + : IconButton( + onPressed: controller.startSearch, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(56); +} diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index 63ecad332..c40062738 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/span_data_type.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; @@ -130,6 +131,7 @@ class SpanCardState extends State { selectedChoice!.isBestCorrection ? ConstructUseTypeEnum.corIGC : ConstructUseTypeEnum.incIGC, + AnalyticsUpdateOrigin.igc, ); } @@ -160,6 +162,7 @@ class SpanCardState extends State { ignoredTokens ?? [], widget.roomId, ConstructUseTypeEnum.ignIGC, + AnalyticsUpdateOrigin.igc, ); } @@ -226,7 +229,9 @@ class WordMatchContent extends StatelessWidget { children: [ const Positioned( top: 40, - child: PointsGainedAnimation(), + child: PointsGainedAnimation( + origin: AnalyticsUpdateOrigin.igc, + ), ), Column( children: [ diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index e76021000..a477efa9b 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -18,12 +18,14 @@ class MultipleChoiceActivity extends StatefulWidget { final PracticeActivityCardState practiceCardController; final PracticeActivityModel currentActivity; final TtsController tts; + final String eventID; const MultipleChoiceActivity({ super.key, required this.practiceCardController, required this.currentActivity, required this.tts, + required this.eventID, }); @override @@ -70,7 +72,6 @@ class MultipleChoiceActivityState extends State { return; } - // #freeze-activity MatrixState.pangeaController.myAnalytics.setState( AnalyticsStream( // note - this maybe should be the activity event id @@ -81,6 +82,7 @@ class MultipleChoiceActivityState extends State { widget.practiceCardController.currentActivity!, widget.practiceCardController.metadata, ), + origin: AnalyticsUpdateOrigin.practiceActivity, ), ); @@ -112,12 +114,12 @@ class MultipleChoiceActivityState extends State { ), ), const SizedBox(height: 8), - // #freeze-activity if (practiceActivity.activityType == ActivityTypeEnum.wordFocusListening) WordAudioButton( text: practiceActivity.content.answer, ttsController: widget.tts, + eventID: widget.eventID, ), ChoicesArray( isLoading: false, diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index dbb98de73..1e97f2fe2 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -1,6 +1,7 @@ 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'; @@ -140,7 +141,6 @@ class PracticeActivityCardState extends State { userL2: pangeaController.languageController.userL2!.langCode, messageText: widget.pangeaMessageEvent.originalSent!.text, tokensWithXP: await targetTokensController.targetTokens( - context, widget.pangeaMessageEvent, ), messageId: widget.pangeaMessageEvent.eventId, @@ -148,6 +148,11 @@ class PracticeActivityCardState extends State { .map((activity) => activity.activityRequestMetaData) .toList(), activityQualityFeedback: activityFeedback, + clientCompatibleActivities: widget.tts.isLanguageFullySupported + ? ActivityTypeEnum.values + : ActivityTypeEnum.values + .where((type) => type != ActivityTypeEnum.wordFocusListening) + .toList(), ), widget.pangeaMessageEvent, ); @@ -217,7 +222,6 @@ class PracticeActivityCardState extends State { currentActivity!, metadata, ), - context, widget.pangeaMessageEvent, ); @@ -298,6 +302,7 @@ class PracticeActivityCardState extends State { practiceCardController: this, currentActivity: currentActivity!, tts: widget.tts, + eventID: widget.pangeaMessageEvent.eventId, ); case ActivityTypeEnum.wordFocusListening: // return WordFocusListeningActivity( @@ -306,6 +311,7 @@ class PracticeActivityCardState extends State { practiceCardController: this, currentActivity: currentActivity!, tts: widget.tts, + eventID: widget.pangeaMessageEvent.eventId, ); // default: // ErrorHandler.logError( @@ -335,7 +341,9 @@ class PracticeActivityCardState extends State { children: [ // Main content const Positioned( - child: PointsGainedAnimation(), + child: PointsGainedAnimation( + origin: AnalyticsUpdateOrigin.practiceActivity, + ), ), if (activityWidget != null) Padding( diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index e358614f3..69be7f6c2 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -4,10 +4,8 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; /// Seperated out the target tokens from the practice activity card /// in order to control the state of the target tokens @@ -19,18 +17,18 @@ class TargetTokensController { /// From the tokens in the message, do a preliminary filtering of which to target /// Then get the construct uses for those tokens Future> targetTokens( - BuildContext context, PangeaMessageEvent pangeaMessageEvent, ) async { if (_targetTokens != null) { return _targetTokens!; } - _targetTokens = await _initialize(context, pangeaMessageEvent); + _targetTokens = await _initialize(pangeaMessageEvent); + final allConstructs = MatrixState + .pangeaController.analytics.analyticsStream.value?.constructs; await updateTokensWithConstructs( - MatrixState.pangeaController.analytics.analyticsStream.value ?? [], - context, + allConstructs ?? [], pangeaMessageEvent, ); @@ -38,34 +36,26 @@ class TargetTokensController { } Future> _initialize( - BuildContext context, PangeaMessageEvent pangeaMessageEvent, ) async { - if (!context.mounted) { - ErrorHandler.logError( - m: 'getTargetTokens called when not mounted', - s: StackTrace.current, - ); - return _targetTokens = []; - } - final tokens = await pangeaMessageEvent .representationByLanguage(pangeaMessageEvent.messageDisplayLangCode) - ?.tokensGlobal(context); + ?.tokensGlobal( + pangeaMessageEvent.senderId, + pangeaMessageEvent.originServerTs, + ); if (tokens == null || tokens.isEmpty) { debugger(when: kDebugMode); return _targetTokens = []; } - return _targetTokens = tokens - .map((token) => token.emptyTokenWithXP) - .toList(); + return _targetTokens = + tokens.map((token) => token.emptyTokenWithXP).toList(); } Future updateTokensWithConstructs( List constructUses, - context, pangeaMessageEvent, ) async { final ConstructListModel constructList = ConstructListModel( @@ -73,12 +63,11 @@ class TargetTokensController { type: null, ); - _targetTokens ??= await _initialize(context, pangeaMessageEvent); + _targetTokens ??= await _initialize(pangeaMessageEvent); for (final token in _targetTokens!) { - // we don't need to do this for tokens that don't have saveVocab set to true - if (!token.token.lemma.saveVocab){ + if (!token.token.lemma.saveVocab) { continue; } diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 2f56299c8..03147ef0d 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -5,11 +5,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class WordAudioButton extends StatefulWidget { final String text; final TtsController ttsController; + final String eventID; const WordAudioButton({ super.key, required this.text, required this.ttsController, + required this.eventID, }); @override @@ -22,42 +24,40 @@ class WordAudioButtonState extends State { @override Widget build(BuildContext context) { debugPrint('build WordAudioButton'); - return Column( - children: [ - IconButton( - icon: const Icon(Icons.play_arrow_outlined), - isSelected: _isPlaying, - selectedIcon: const Icon(Icons.pause_outlined), - color: _isPlaying ? Colors.white : null, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - _isPlaying - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primaryContainer, - ), - ), - tooltip: - _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, - onPressed: () async { - if (_isPlaying) { - await widget.ttsController.tts.stop(); - if (mounted) { - setState(() => _isPlaying = false); - } - } else { - if (mounted) { - setState(() => _isPlaying = true); - } - await widget.ttsController.speak(widget.text); - if (mounted) { - setState(() => _isPlaying = false); - } - } - }, // Disable button if language isn't supported + return IconButton( + icon: const Icon(Icons.play_arrow_outlined), + isSelected: _isPlaying, + selectedIcon: const Icon(Icons.pause_outlined), + color: _isPlaying ? Colors.white : null, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + _isPlaying + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primaryContainer, ), - // #freeze-activity - widget.ttsController.missingVoiceButton, - ], + ), + tooltip: + _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, + onPressed: () async { + if (_isPlaying) { + await widget.ttsController.tts.stop(); + if (mounted) { + setState(() => _isPlaying = false); + } + } else { + if (mounted) { + setState(() => _isPlaying = true); + } + await widget.ttsController.tryToSpeak( + widget.text, + context, + widget.eventID, + ); + if (mounted) { + setState(() => _isPlaying = false); + } + } + }, // Disable button if language isn't supported ); } } diff --git a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart index 8e22aced8..810074a76 100644 --- a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -79,6 +79,7 @@ class WordFocusListeningActivityState widget.practiceCardController.currentActivity!, widget.practiceCardController.metadata, ), + origin: AnalyticsUpdateOrigin.practiceActivity, ), ); setState(() {