diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 490f6c91d..9921f8c28 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4127,5 +4127,6 @@ "createSpace": "Create space", "createChat": "Create chat", "error520Title": "Please try again.", - "error520Desc": "Sorry, we could not understand your message..." + "error520Desc": "Sorry, we could not understand your message...", + "translationChoicesBody": "Click and hold an option for a hint." } \ No newline at end of file diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index b1174fd2c..bee8a1af8 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,9 +1,8 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pages/chat/input_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; -import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -34,7 +33,7 @@ class ChatInputRow extends StatelessWidget { controller.pangeaController.languageController.activeL2Model(); String hintText() { - if (controller.choreographer.choreoMode == ChoreoMode.it) { + if (controller.choreographer.itController.willOpen) { return L10n.of(context)!.buildTranslation; } return activel1 != null && @@ -322,10 +321,7 @@ class ChatInputRow extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 0.0), - // #Pangea - // child: InputBar( - child: InputBarWrapper( - // Pangea# + child: InputBar( room: controller.room, minLines: 1, maxLines: 8, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 405e16418..4d04d3051 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; -import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; @@ -13,6 +12,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart'; +import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; @@ -498,7 +498,9 @@ class ChatView extends StatelessWidget { ), ReactionsPicker(controller), ReplyDisplay(controller), - ChatInputRow(controller), + ChatInputRowWrapper( + controller: controller, + ), ChatEmojiPicker(controller), ], ), diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 237697b40..aae7104d4 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -41,6 +41,7 @@ class IgcController { final IGCRequestBody reqBody = IGCRequestBody( fullText: choreographer.currentText, + userId: choreographer.pangeaController.userController.userId!, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection, diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 3187d4a6a..74bce2f92 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -54,6 +55,23 @@ class ITController { choreographer.setState(); } + bool _closingHint = false; + Duration get animationSpeed => (_closingHint || !_willOpen) + ? const Duration(milliseconds: 500) + : const Duration(milliseconds: 2000); + + void closeHint() { + _closingHint = true; + final String hintKey = InlineInstructions.translationChoices.toString(); + final instructionsController = choreographer.pangeaController.instructions; + instructionsController.turnOffInstruction(hintKey); + instructionsController.updateEnableInstructions(hintKey, true); + choreographer.setState(); + Future.delayed(const Duration(milliseconds: 500), () { + _closingHint = false; + }); + } + Future initializeIT(ITStartData itStartData) async { _willOpen = true; Future.delayed(const Duration(microseconds: 100), () { diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 28b0f8bd8..f7d928f0c 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -7,7 +7,9 @@ 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/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -45,12 +47,16 @@ class ITBarState extends State { super.dispose(); } + bool get instructionsTurnedOff => + widget.choreographer.pangeaController.instructions + .wereInstructionsTurnedOff( + InlineInstructions.translationChoices.toString(), + ); + @override Widget build(BuildContext context) { return AnimatedSize( - duration: itController.willOpen - ? const Duration(milliseconds: 2000) - : const Duration(milliseconds: 500), + duration: itController.animationSpeed, curve: Curves.fastOutSlowIn, clipBehavior: Clip.none, child: !itController.willOpen @@ -58,9 +64,7 @@ class ITBarState extends State { : CompositedTransformTarget( link: widget.choreographer.itBarLinkAndKey.link, child: AnimatedOpacity( - duration: itController.willOpen - ? const Duration(milliseconds: 2000) - : const Duration(milliseconds: 500), + duration: itController.animationSpeed, opacity: itController.willOpen ? 1.0 : 0.0, child: Container( key: widget.choreographer.itBarLinkAndKey.key, @@ -109,6 +113,12 @@ class ITBarState extends State { // const SizedBox(height: 40.0), OriginalText(controller: itController), const SizedBox(height: 7.0), + if (!instructionsTurnedOff) + InlineTooltip( + body: InlineInstructions.translationChoices + .body(context), + onClose: itController.closeHint, + ), IntrinsicHeight( child: Container( constraints: @@ -151,6 +161,7 @@ class ITBarState extends State { ), ), ), + // ), ), ); } @@ -305,7 +316,11 @@ class ITChoices extends StatelessWidget { chosenContinuance: controller.currentITStep!.continuances[index].text, bestContinuance: controller.currentITStep!.best.text, - feedbackLang: controller.targetLangCode, + // TODO: we want this to eventually switch between target and source lang, + // based on the learner's proficiency - maybe with the words involved in the translation + // maybe overall. For now, we'll just use the source lang. + feedbackLang: controller.choreographer.l1Lang?.langCode ?? + controller.sourceLangCode, sourceTextLang: controller.sourceLangCode, targetLang: controller.targetLangCode, ), diff --git a/lib/pangea/choreographer/widgets/it_feedback_card.dart b/lib/pangea/choreographer/widgets/it_feedback_card.dart index f959715ce..06f2493d1 100644 --- a/lib/pangea/choreographer/widgets/it_feedback_card.dart +++ b/lib/pangea/choreographer/widgets/it_feedback_card.dart @@ -146,8 +146,11 @@ class ITFeedbackCardView extends StatelessWidget { controller.res!.text, style: BotStyle.text(context), ), - // if res is not null, show a button to translate the text - if (controller.res != null && controller.translatedFeedback == null) + // if res is not null and feedback not in the userL1, show a button to translate the text + if (controller.res != null && + controller.translatedFeedback == null && + controller.widget.req.feedbackLang != + controller.controller.languageController.userL1?.langCode) Column( children: [ const SizedBox(height: 10), diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart index 4e12c12b8..48544925e 100644 --- a/lib/pangea/enum/instructions_enum.dart +++ b/lib/pangea/enum/instructions_enum.dart @@ -58,6 +58,7 @@ extension InstructionsEnumExtension on InstructionsEnum { enum InlineInstructions { speechToText, l1Translation, + translationChoices, } extension InlineInstructionsExtension on InlineInstructions { @@ -67,6 +68,21 @@ extension InlineInstructionsExtension on InlineInstructions { return L10n.of(context)!.speechToTextBody; case InlineInstructions.l1Translation: return L10n.of(context)!.l1TranslationBody; + case InlineInstructions.translationChoices: + return L10n.of(context)!.translationChoicesBody; + } + } + + bool get toggledOff { + final instructionSettings = + MatrixState.pangeaController.userController.profile.instructionSettings; + switch (this) { + case InlineInstructions.speechToText: + return instructionSettings.showedSpeechToTextTooltip; + case InlineInstructions.l1Translation: + return instructionSettings.showedL1TranslationTooltip; + case InlineInstructions.translationChoices: + return instructionSettings.showedTranslationChoicesTooltip; } } } diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 180422576..24db180dc 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -186,11 +186,18 @@ class UserInstructions { bool showedBlurMeansTranslate; bool showedTooltipInstructions; + bool showedSpeechToTextTooltip; + bool showedL1TranslationTooltip; + bool showedTranslationChoicesTooltip; + UserInstructions({ this.showedItInstructions = false, this.showedClickMessage = false, this.showedBlurMeansTranslate = false, this.showedTooltipInstructions = false, + this.showedSpeechToTextTooltip = false, + this.showedL1TranslationTooltip = false, + this.showedTranslationChoicesTooltip = false, }); factory UserInstructions.fromJson(Map json) => @@ -203,6 +210,12 @@ class UserInstructions { json[InstructionsEnum.blurMeansTranslate.toString()] ?? false, showedTooltipInstructions: json[InstructionsEnum.tooltipInstructions.toString()] ?? false, + showedL1TranslationTooltip: + json[InlineInstructions.l1Translation.toString()] ?? false, + showedTranslationChoicesTooltip: + json[InlineInstructions.translationChoices.toString()] ?? false, + showedSpeechToTextTooltip: + json[InlineInstructions.speechToText.toString()] ?? false, ); Map toJson() { @@ -213,6 +226,12 @@ class UserInstructions { showedBlurMeansTranslate; data[InstructionsEnum.tooltipInstructions.toString()] = showedTooltipInstructions; + data[InlineInstructions.l1Translation.toString()] = + showedL1TranslationTooltip; + data[InlineInstructions.translationChoices.toString()] = + showedTranslationChoicesTooltip; + data[InlineInstructions.speechToText.toString()] = + showedSpeechToTextTooltip; return data; } @@ -238,6 +257,21 @@ class UserInstructions { ?.content[InstructionsEnum.tooltipInstructions.toString()] as bool?) ?? false, + showedL1TranslationTooltip: + (accountData[InlineInstructions.l1Translation.toString()] + ?.content[InlineInstructions.l1Translation.toString()] + as bool?) ?? + false, + showedTranslationChoicesTooltip: (accountData[ + InlineInstructions.translationChoices.toString()] + ?.content[InlineInstructions.translationChoices.toString()] + as bool?) ?? + false, + showedSpeechToTextTooltip: + (accountData[InlineInstructions.speechToText.toString()] + ?.content[InlineInstructions.speechToText.toString()] + as bool?) ?? + false, ); } } diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 33abb4deb..bb5c27062 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -129,6 +129,7 @@ class IGCRequestBody { String userL2; bool enableIT; bool enableIGC; + String userId; List prevMessages; IGCRequestBody({ @@ -137,6 +138,7 @@ class IGCRequestBody { required this.userL2, required this.enableIGC, required this.enableIT, + required this.userId, required this.prevMessages, }); @@ -146,6 +148,7 @@ class IGCRequestBody { ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, + ModelKey.userId: userId, ModelKey.prevMessages: jsonEncode(prevMessages.map((x) => x.toJson()).toList()), }; diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart index 21fc7321a..f82a11682 100644 --- a/lib/pangea/utils/inline_tooltip.dart +++ b/lib/pangea/utils/inline_tooltip.dart @@ -26,7 +26,7 @@ class InlineTooltip extends StatelessWidget { onPressed: onClose, ), ), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Theme.of(context).colorScheme.primary.withAlpha(20), diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 165bfa460..78dab6f6c 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -25,9 +25,15 @@ class InstructionsController { final Map _instructionsShown = {}; /// Returns true if the user requested this popup not be shown again - bool? toggledOff(String key) => InstructionsEnum.values - .firstWhereOrNull((value) => value.toString() == key) - ?.toggledOff; + bool? toggledOff(String key) { + final bool? instruction = InstructionsEnum.values + .firstWhereOrNull((value) => value.toString() == key) + ?.toggledOff; + final bool? tooltip = InlineInstructions.values + .firstWhereOrNull((value) => value.toString() == key) + ?.toggledOff; + return instruction ?? tooltip; + } InstructionsController(PangeaController pangeaController) { _pangeaController = pangeaController; @@ -58,6 +64,15 @@ class InstructionsController { if (key == InstructionsEnum.tooltipInstructions.toString()) { profile.instructionSettings.showedTooltipInstructions = value; } + if (key == InlineInstructions.speechToText.toString()) { + profile.instructionSettings.showedSpeechToTextTooltip = value; + } + if (key == InlineInstructions.l1Translation.toString()) { + profile.instructionSettings.showedL1TranslationTooltip = value; + } + if (key == InlineInstructions.translationChoices.toString()) { + profile.instructionSettings.showedTranslationChoicesTooltip = value; + } return profile; }); } diff --git a/lib/pangea/widgets/chat/input_bar_wrapper.dart b/lib/pangea/widgets/chat/input_bar_wrapper.dart index 9441312bd..6c6f80218 100644 --- a/lib/pangea/widgets/chat/input_bar_wrapper.dart +++ b/lib/pangea/widgets/chat/input_bar_wrapper.dart @@ -1,48 +1,23 @@ import 'dart:async'; -import 'dart:typed_data'; -import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_input_row.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; -class InputBarWrapper extends StatefulWidget { - final Room room; - final int? minLines; - final int? maxLines; - final TextInputType? keyboardType; - final TextInputAction? textInputAction; - final ValueChanged? onSubmitted; - final ValueChanged? onSubmitImage; - final FocusNode? focusNode; - final PangeaTextController? controller; - final InputDecoration? decoration; - final ValueChanged? onChanged; - final bool? autofocus; - final bool readOnly; +class ChatInputRowWrapper extends StatefulWidget { + final ChatController controller; - const InputBarWrapper({ - required this.room, - this.minLines, - this.maxLines, - this.keyboardType, - this.onSubmitted, - this.onSubmitImage, - this.focusNode, - this.controller, - this.decoration, - this.onChanged, - this.autofocus, - this.textInputAction, - this.readOnly = false, + const ChatInputRowWrapper({ + required this.controller, super.key, }); @override - State createState() => InputBarWrapperState(); + State createState() => ChatInputRowWrapperState(); } -class InputBarWrapperState extends State { +class ChatInputRowWrapperState extends State { StreamSubscription? _choreoSub; String _currentText = ''; @@ -50,7 +25,7 @@ class InputBarWrapperState extends State { void initState() { // Rebuild the widget each time there's an update from choreo _choreoSub = - widget.controller?.choreographer.stateListener.stream.listen((_) { + widget.controller.choreographer.stateListener.stream.listen((_) { setState(() {}); }); super.initState(); @@ -63,10 +38,6 @@ class InputBarWrapperState extends State { } void refreshOnChange(String text) { - if (widget.onChanged != null) { - widget.onChanged!(text); - } - final bool decreasedFromMaxLength = _currentText.length >= PangeaTextController.maxLength && text.length < PangeaTextController.maxLength; @@ -81,21 +52,5 @@ class InputBarWrapperState extends State { } @override - Widget build(BuildContext context) { - return InputBar( - room: widget.room, - minLines: widget.minLines, - maxLines: widget.maxLines, - keyboardType: widget.keyboardType, - onSubmitted: widget.onSubmitted, - onSubmitImage: widget.onSubmitImage, - focusNode: widget.focusNode, - controller: widget.controller, - decoration: widget.decoration, - onChanged: refreshOnChange, - autofocus: widget.autofocus, - textInputAction: widget.textInputAction, - readOnly: widget.readOnly, - ); - } + Widget build(BuildContext context) => ChatInputRow(widget.controller); }