From 2bc421e6e7c26f30e506af2e1578462b24d5bf19 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 10 Oct 2024 15:57:50 -0400 Subject: [PATCH 001/115] make popup dimensions dynamic --- .../controllers/choreographer.dart | 7 +- .../controllers/igc_controller.dart | 3 +- lib/pangea/choreographer/widgets/it_bar.dart | 3 +- lib/pangea/utils/instructions.dart | 17 +- lib/pangea/utils/overlay.dart | 229 +++++++++--------- .../common_widgets/overlay_container.dart | 19 +- .../widgets/igc/pangea_text_controller.dart | 14 +- lib/pangea/widgets/igc/span_card.dart | 82 +++---- 8 files changed, 187 insertions(+), 187 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 9053d3353..cc016b1ca 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -74,8 +74,11 @@ class Choreographer { CanSendStatus.showPaywall) { OverlayUtil.showPositionedCard( context: context, - cardToShow: const PaywallCard(), - cardSize: const Size(325, 325), + cardToShow: PaywallCard( + chatController: chatController, + ), + maxHeight: 325, + maxWidth: 325, transformTargetId: inputTransformTargetKey, ); return; diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 4beea095f..e835359e6 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -124,7 +124,8 @@ class IgcController { ), roomId: choreographer.roomId, ), - cardSize: match.isITStart ? const Size(350, 260) : const Size(400, 400), + maxHeight: match.isITStart ? 260 : 400, + maxWidth: match.isITStart ? 350 : 400, transformTargetId: choreographer.inputTransformTargetKey, ); } diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index bfc4a174d..718e28beb 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -339,7 +339,8 @@ class ITChoices extends StatelessWidget { ), choiceFeedback: choiceFeedback, ), - cardSize: const Size(300, 300), + maxHeight: 300, + maxWidth: 300, borderColor: borderColor, transformTargetId: controller.choreographer.itBarTransformTargetKey, backDropToDismiss: false, diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 9011f14ed..31b552384 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -106,21 +106,18 @@ class InstructionsController { onClose: () => {_instructionsClosed[key.toString()] = true}, ), const SizedBox(height: 10.0), - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - key.body(context), - style: botStyle, - ), - ), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + key.body(context), + style: botStyle, ), ), if (showToggle) InstructionsToggle(instructionsKey: key), ], ), - cardSize: const Size(300.0, 300.0), + maxHeight: 300, + maxWidth: 300, transformTargetId: transformTargetKey, closePrevOverlay: false, ), diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index cb6ed626a..5bf8a5823 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -1,8 +1,6 @@ import 'dart:developer'; -import 'dart:math'; import 'dart:ui'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; import 'package:flutter/foundation.dart'; @@ -22,10 +20,8 @@ class OverlayUtil { required BuildContext context, required Widget child, required String transformTargetId, - // Size? size, double? width, double? height, - Offset? offset, backDropToDismiss = true, blurBackground = false, Color? borderColor, @@ -63,13 +59,13 @@ class OverlayUtil { child: (position != OverlayPositionEnum.transform) ? child : CompositedTransformFollower( - targetAnchor: targetAnchor ?? Alignment.topLeft, - followerAnchor: followerAnchor ?? Alignment.topLeft, + targetAnchor: targetAnchor ?? Alignment.topCenter, + followerAnchor: + followerAnchor ?? Alignment.bottomCenter, link: MatrixState.pAnyState .layerLinkAndKey(transformTargetId) .link, showWhenUnlinked: false, - offset: offset ?? Offset.zero, child: child, ), ), @@ -89,8 +85,9 @@ class OverlayUtil { static showPositionedCard({ required BuildContext context, required Widget cardToShow, - required Size cardSize, required String transformTargetId, + required double maxHeight, + required double maxWidth, backDropToDismiss = true, Color? borderColor, bool closePrevOverlay = true, @@ -103,11 +100,6 @@ class OverlayUtil { return; } - final Offset cardOffset = _calculateCardOffset( - cardSize: cardSize, - transformTargetContext: layerLinkAndKey.key.currentContext!, - ); - final Widget child = Material( borderOnForeground: false, color: Colors.transparent, @@ -115,16 +107,15 @@ class OverlayUtil { child: OverlayContainer( cardToShow: cardToShow, borderColor: borderColor, + maxHeight: maxHeight, + maxWidth: maxWidth, ), ); showOverlay( context: context, child: child, - width: cardSize.width, - height: cardSize.height, transformTargetId: transformTargetId, - offset: cardOffset, backDropToDismiss: backDropToDismiss, borderColor: borderColor, closePrevOverlay: closePrevOverlay, @@ -135,73 +126,73 @@ class OverlayUtil { } } - /// calculates the card offset relative to the target - /// identified by [transformTargetKey] - static Offset _calculateCardOffset({ - required Size cardSize, - required BuildContext transformTargetContext, - final double minPadding = 10.0, - }) { - // debugger(when: kDebugMode); - //Note: assumes overlay in chatview - final OverlayConstraints constraints = - ChatViewConstraints(transformTargetContext); + // /// calculates the card offset relative to the target + // /// identified by [transformTargetKey] + // static Offset _calculateCardOffset({ + // required Size cardSize, + // required BuildContext transformTargetContext, + // final double minPadding = 10.0, + // }) { + // // debugger(when: kDebugMode); + // //Note: assumes overlay in chatview + // final OverlayConstraints constraints = + // ChatViewConstraints(transformTargetContext); - final RenderObject? targetRenderBox = - transformTargetContext.findRenderObject(); - if (targetRenderBox == null) return Offset.zero; - final Offset transformTargetOffset = - (targetRenderBox as RenderBox).localToGlobal(Offset.zero); - final Size transformTargetSize = targetRenderBox.size; + // final RenderObject? targetRenderBox = + // transformTargetContext.findRenderObject(); + // if (targetRenderBox == null) return Offset.zero; + // final Offset transformTargetOffset = + // (targetRenderBox as RenderBox).localToGlobal(Offset.zero); + // final Size transformTargetSize = targetRenderBox.size; - // ideally horizontally centered on target - double dx = transformTargetSize.width / 2 - cardSize.width / 2; - // make sure it's not off the left edge of the screen - // if transformTargetOffset.dx + dc < constraints.x0 + minPadding + // // ideally horizontally centered on target + // double dx = transformTargetSize.width / 2 - cardSize.width / 2; + // // make sure it's not off the left edge of the screen + // // if transformTargetOffset.dx + dc < constraints.x0 + minPadding - if (transformTargetOffset.dx + dx < minPadding + constraints.x0) { - debugPrint("setting dx"); - dx = minPadding + constraints.x0 - transformTargetOffset.dx; - } - // make sure it's not off the right edge of the screen - if (transformTargetOffset.dx + dx + cardSize.width + minPadding > - constraints.x1) { - dx = constraints.x1 - - transformTargetOffset.dx - - cardSize.width - - minPadding; - } + // if (transformTargetOffset.dx + dx < minPadding + constraints.x0) { + // debugPrint("setting dx"); + // dx = minPadding + constraints.x0 - transformTargetOffset.dx; + // } + // // make sure it's not off the right edge of the screen + // if (transformTargetOffset.dx + dx + cardSize.width + minPadding > + // constraints.x1) { + // dx = constraints.x1 - + // transformTargetOffset.dx - + // cardSize.width - + // minPadding; + // } - // if there's more room above target, - // put the card there - // else, - // put it below - // debugPrint( - // "transformTargetOffset.dx ${transformTargetOffset.dx} transformTargetOffset.dy ${transformTargetOffset.dy}"); - // debugPrint( - // "transformTargetSize.width ${transformTargetSize.width} transformTargetSize.height ${transformTargetSize.height}"); - double dy = transformTargetOffset.dy > - constraints.y1 - - transformTargetOffset.dy - - transformTargetSize.height - ? -cardSize.height - minPadding - : transformTargetSize.height + minPadding; - // make sure it's not off the top edge of the screen - if (dy < minPadding + constraints.y0 - transformTargetOffset.dy) { - dy = minPadding + constraints.y0 - transformTargetOffset.dy; - } - // make sure it's not off the bottom edge of the screen - if (transformTargetOffset.dy + dy + cardSize.height + minPadding > - constraints.y1) { - dy = constraints.y1 - - transformTargetOffset.dy - - cardSize.height - - minPadding; - } - // debugPrint("dx $dx dy $dy"); + // // if there's more room above target, + // // put the card there + // // else, + // // put it below + // // debugPrint( + // // "transformTargetOffset.dx ${transformTargetOffset.dx} transformTargetOffset.dy ${transformTargetOffset.dy}"); + // // debugPrint( + // // "transformTargetSize.width ${transformTargetSize.width} transformTargetSize.height ${transformTargetSize.height}"); + // double dy = transformTargetOffset.dy > + // constraints.y1 - + // transformTargetOffset.dy - + // transformTargetSize.height + // ? -cardSize.height - minPadding + // : transformTargetSize.height + minPadding; + // // make sure it's not off the top edge of the screen + // if (dy < minPadding + constraints.y0 - transformTargetOffset.dy) { + // dy = minPadding + constraints.y0 - transformTargetOffset.dy; + // } + // // make sure it's not off the bottom edge of the screen + // if (transformTargetOffset.dy + dy + cardSize.height + minPadding > + // constraints.y1) { + // dy = constraints.y1 - + // transformTargetOffset.dy - + // cardSize.height - + // minPadding; + // } + // // debugPrint("dx $dx dy $dy"); - return Offset(dx, dy); - } + // return Offset(dx, dy); + // } static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty; } @@ -250,48 +241,48 @@ class TransparentBackdrop extends StatelessWidget { } } -/// global coordinates that the overlay should stay inside -abstract class OverlayConstraints { - late double x0; - late double y0; - late double x1; - late double y1; -} +// /// global coordinates that the overlay should stay inside +// abstract class OverlayConstraints { +// late double x0; +// late double y0; +// late double x1; +// late double y1; +// } -class ChatViewConstraints implements OverlayConstraints { - @override - late double x0; - @override - late double y0; - @override - late double x1; - @override - late double y1; +// class ChatViewConstraints implements OverlayConstraints { +// @override +// late double x0; +// @override +// late double y0; +// @override +// late double x1; +// @override +// late double y1; - ChatViewConstraints(BuildContext context) { - final MediaQueryData mediaQueryData = - MediaQuery.of(Scaffold.of(context).context); - final bool isColumnMode = FluffyThemes.isColumnMode(context); +// ChatViewConstraints(BuildContext context) { +// final MediaQueryData mediaQueryData = +// MediaQuery.of(Scaffold.of(context).context); +// final bool isColumnMode = FluffyThemes.isColumnMode(context); - x0 = isColumnMode - ? AppConfig.columnWidth + 70.0 - : max(mediaQueryData.viewPadding.left, mediaQueryData.viewInsets.left); - y0 = max(mediaQueryData.viewPadding.top, mediaQueryData.viewInsets.top); - x1 = mediaQueryData.size.width - - max(mediaQueryData.viewPadding.right, mediaQueryData.viewInsets.right); - y1 = mediaQueryData.size.height - - max( - mediaQueryData.viewPadding.bottom, - mediaQueryData.viewInsets.bottom, - ); +// x0 = isColumnMode +// ? AppConfig.columnWidth + 70.0 +// : max(mediaQueryData.viewPadding.left, mediaQueryData.viewInsets.left); +// y0 = max(mediaQueryData.viewPadding.top, mediaQueryData.viewInsets.top); +// x1 = mediaQueryData.size.width - +// max(mediaQueryData.viewPadding.right, mediaQueryData.viewInsets.right); +// y1 = mediaQueryData.size.height - +// max( +// mediaQueryData.viewPadding.bottom, +// mediaQueryData.viewInsets.bottom, +// ); - // https://medium.com/flutter-community/a-flutter-guide-to-visual-overlap-padding-viewpadding-and-viewinsets-a63e214be6e8 - // debugPrint( - // "viewInsets ${mediaQueryData.viewInsets.left} ${mediaQueryData.viewInsets.top} ${mediaQueryData.viewInsets.right} ${mediaQueryData.viewInsets.bottom}"); - // debugPrint( - // "padding ${mediaQueryData.padding.left} ${mediaQueryData.padding.top} ${mediaQueryData.padding.right} ${mediaQueryData.padding.bottom}"); - // debugPrint( - // "viewPadding ${mediaQueryData.viewPadding.left} ${mediaQueryData.viewPadding.top} ${mediaQueryData.viewPadding.right} ${mediaQueryData.viewPadding.bottom}"); - // debugPrint("chatViewConstraints x0: $x0 y0: $y0 x1: $x1 y1: $y1"); - } -} +// // https://medium.com/flutter-community/a-flutter-guide-to-visual-overlap-padding-viewpadding-and-viewinsets-a63e214be6e8 +// // debugPrint( +// // "viewInsets ${mediaQueryData.viewInsets.left} ${mediaQueryData.viewInsets.top} ${mediaQueryData.viewInsets.right} ${mediaQueryData.viewInsets.bottom}"); +// // debugPrint( +// // "padding ${mediaQueryData.padding.left} ${mediaQueryData.padding.top} ${mediaQueryData.padding.right} ${mediaQueryData.padding.bottom}"); +// // debugPrint( +// // "viewPadding ${mediaQueryData.viewPadding.left} ${mediaQueryData.viewPadding.top} ${mediaQueryData.viewPadding.right} ${mediaQueryData.viewPadding.bottom}"); +// // debugPrint("chatViewConstraints x0: $x0 y0: $y0 x1: $x1 y1: $y1"); +// } +// } diff --git a/lib/pangea/widgets/common_widgets/overlay_container.dart b/lib/pangea/widgets/common_widgets/overlay_container.dart index 4fc64fb3d..5e2991be7 100644 --- a/lib/pangea/widgets/common_widgets/overlay_container.dart +++ b/lib/pangea/widgets/common_widgets/overlay_container.dart @@ -2,14 +2,16 @@ import 'package:flutter/material.dart'; class OverlayContainer extends StatelessWidget { final Widget cardToShow; - final Size cardSize; final Color? borderColor; + final double maxHeight; + final double maxWidth; const OverlayContainer({ super.key, required this.cardToShow, - this.cardSize = const Size(300.0, 300.0), this.borderColor, + required this.maxHeight, + required this.maxWidth, }); @override @@ -28,14 +30,17 @@ class OverlayContainer extends StatelessWidget { ), ), constraints: BoxConstraints( - maxWidth: cardSize.width, - maxHeight: cardSize.height, - minWidth: cardSize.width, - minHeight: cardSize.height, + maxWidth: maxWidth, + maxHeight: maxHeight, ), //PTODO - position card above input/message // margin: const EdgeInsets.all(10), - child: cardToShow, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [cardToShow], + ), + ), ); } } diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index 63c5b3f94..378c33ad7 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -54,8 +54,11 @@ class PangeaTextController extends TextEditingController { text.isNotEmpty) { OverlayUtil.showPositionedCard( context: context, - cardToShow: const PaywallCard(), - cardSize: const Size(325, 325), + cardToShow: PaywallCard( + chatController: choreographer.chatController, + ), + maxHeight: 325, + maxWidth: 325, transformTargetId: choreographer.inputTransformTargetKey, ); } @@ -112,10 +115,11 @@ class PangeaTextController extends TextEditingController { if (cardToShow != null) { OverlayUtil.showPositionedCard( context: context, - cardSize: matchIndex != -1 && + maxHeight: matchIndex != -1 && choreographer.igc.igcTextData!.matches[matchIndex].isITStart - ? const Size(350, 260) - : const Size(350, 400), + ? 260 + : 400, + maxWidth: 350, cardToShow: cardToShow, transformTargetId: choreographer.inputTransformTargetKey, ); diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index 816e8ed15..57991a1a9 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -237,49 +237,47 @@ class WordMatchContent extends StatelessWidget { ? controller.currentExpression : BotExpression.addled, ), - Expanded( - child: Scrollbar( + Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( controller: scrollController, - thumbVisibility: true, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // const SizedBox(height: 10.0), - // if (matchCopy.description != null) - // Padding( - // padding: const EdgeInsets.only(), - // child: Text( - // matchCopy.description!, - // style: BotStyle.text(context), - // ), - // ), - const SizedBox(height: 8), - if (!controller.widget.scm.pangeaMatch!.isITStart) - ChoicesArray( - originalSpan: - controller.widget.scm.pangeaMatch!.matchContent, - isLoading: controller.fetchingData, - choices: - controller.widget.scm.pangeaMatch!.match.choices - ?.map( - (e) => Choice( - text: e.value, - color: e.selected ? e.type.color : null, - isGold: e.type.name == 'bestCorrection', - ), - ) - .toList(), - onPressed: controller.onChoiceSelect, - uniqueKeyForLayerLink: (int index) => - "wordMatch$index", - selectedChoiceIndex: controller.selectedChoiceIndex, - ), - const SizedBox(height: 12), - PromptAndFeedback(controller: controller), - ], - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // const SizedBox(height: 10.0), + // if (matchCopy.description != null) + // Padding( + // padding: const EdgeInsets.only(), + // child: Text( + // matchCopy.description!, + // style: BotStyle.text(context), + // ), + // ), + const SizedBox(height: 8), + if (!controller.widget.scm.pangeaMatch!.isITStart) + ChoicesArray( + originalSpan: + controller.widget.scm.pangeaMatch!.matchContent, + isLoading: controller.fetchingData, + choices: + controller.widget.scm.pangeaMatch!.match.choices + ?.map( + (e) => Choice( + text: e.value, + color: e.selected ? e.type.color : null, + isGold: e.type.name == 'bestCorrection', + ), + ) + .toList(), + onPressed: controller.onChoiceSelect, + uniqueKeyForLayerLink: (int index) => + "wordMatch$index", + selectedChoiceIndex: controller.selectedChoiceIndex, + ), + const SizedBox(height: 12), + PromptAndFeedback(controller: controller), + ], ), ), ), From 4ddd28b7fe6336c7bd0b610fff9954039c14ee1c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 10 Oct 2024 15:59:28 -0400 Subject: [PATCH 002/115] makes toolbar have dynamic width to fit around content --- .../chat/message_selection_overlay.dart | 33 ++++------- lib/pangea/widgets/chat/message_toolbar.dart | 56 ++++++++++--------- lib/pangea/widgets/igc/paywall_card.dart | 4 ++ 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 428d36e79..1d6e5c2a1 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -373,24 +373,16 @@ class MessageOverlayController extends State // the default spacing between the side of the screen and the message bubble final double messageMargin = pangeaMessageEvent.ownMessage ? Avatar.defaultSize + 16 : 8; - - // the actual spacing between the side of the screen and - // the message bubble, accounts for wide screen - double extraChatSpace = FluffyThemes.isColumnMode(context) - ? ((screenWidth - - (FluffyThemes.columnWidth * 3.5) - - FluffyThemes.navRailWidth) / - 2) + - messageMargin - : messageMargin; - - if (extraChatSpace < messageMargin) { - extraChatSpace = messageMargin; - } + final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; + final chatViewWidth = screenWidth - + (FluffyThemes.isColumnMode(context) + ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) + : 0); + final maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin; final overlayMessage = Container( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, + constraints: BoxConstraints( + maxWidth: maxWidth, ), child: Material( type: MaterialType.transparency, @@ -439,21 +431,20 @@ class MessageOverlayController extends State ), ); - final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; final columnOffset = FluffyThemes.isColumnMode(context) ? FluffyThemes.columnWidth + FluffyThemes.navRailWidth : 0; - final double leftPadding = widget._pangeaMessageEvent.ownMessage - ? extraChatSpace + final double? leftPadding = widget._pangeaMessageEvent.ownMessage + ? null : messageOffset!.dx - horizontalPadding - columnOffset; - final double rightPadding = widget._pangeaMessageEvent.ownMessage + final double? rightPadding = widget._pangeaMessageEvent.ownMessage ? screenWidth - messageOffset!.dx - messageSize!.width - horizontalPadding - : extraChatSpace; + : null; final positionedOverlayMessage = _overlayPositionAnimation == null ? Positioned( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 704338764..b420ddcaa 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -120,34 +120,40 @@ class MessageToolbarState extends State { .layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar') .key, type: MaterialType.transparency, - child: Column( + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), - ), - constraints: const BoxConstraints( - maxHeight: AppConfig.toolbarMaxHeight, - ), - child: Row( - children: [ - Expanded( - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent, - ), + Column( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: + Theme.of(context).colorScheme.primary.withOpacity(0.5), ), + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + ), + constraints: const BoxConstraints( + maxHeight: AppConfig.toolbarMaxHeight, ), - ], - ), + // child: Row( + // children: [ + // Expanded( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent, + ), + ), + // ), + // ], + // ), + ), + ], ), ], ), diff --git a/lib/pangea/widgets/igc/paywall_card.dart b/lib/pangea/widgets/igc/paywall_card.dart index a2c35ff86..20b5fcefe 100644 --- a/lib/pangea/widgets/igc/paywall_card.dart +++ b/lib/pangea/widgets/igc/paywall_card.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; import 'package:fluffychat/pangea/widgets/igc/card_header.dart'; @@ -7,8 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class PaywallCard extends StatelessWidget { + final ChatController chatController; const PaywallCard({ super.key, + required this.chatController, }); @override @@ -69,6 +72,7 @@ class PaywallCard extends StatelessWidget { width: double.infinity, child: TextButton( onPressed: () { + chatController.clearSelectedEvents(); MatrixState.pangeaController.subscriptionController .showPaywall(context); }, From ac80e6217cd22c515b10dbc020da49490af874bb Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:53:34 -0400 Subject: [PATCH 003/115] Audio section widget (#744) first draft of word focus listening activities using text to speech library --- android/app/src/main/AndroidManifest.xml | 6 + assets/l10n/intl_en.arb | 4 + lib/pages/chat/events/audio_player.dart | 180 ++++++-- ...actice_activity_generation_controller.dart | 2 +- .../text_to_speech_controller.dart | 97 ++++- .../activity_display_instructions_enum.dart | 11 +- lib/pangea/enum/activity_type_enum.dart | 10 +- lib/pangea/enum/construct_use_type_enum.dart | 97 ++--- .../extensions/pangea_event_extension.dart | 42 ++ .../pangea_message_event.dart | 146 +++---- lib/pangea/models/headwords.dart | 390 +++++++++--------- lib/pangea/models/pangea_token_model.dart | 4 - .../message_activity_request.dart | 16 +- .../multiple_choice_activity_model.dart | 8 +- .../practice_activity_model.dart | 137 +----- .../widgets/chat/message_audio_card.dart | 168 ++++++-- .../chat/message_selection_overlay.dart | 25 +- lib/pangea/widgets/chat/message_toolbar.dart | 1 + .../widgets/chat/message_toolbar_buttons.dart | 48 ++- .../widgets/chat/missing_voice_button.dart | 61 +++ lib/pangea/widgets/chat/tts_controller.dart | 77 ++++ .../multiple_choice_activity.dart | 26 +- .../practice_activity_card.dart | 9 +- .../target_tokens_controller.dart | 20 +- .../practice_activity/word_audio_button.dart | 69 ++++ .../word_focus_listening_activity.dart | 173 ++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 16 + pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 31 files changed, 1239 insertions(+), 612 deletions(-) create mode 100644 lib/pangea/widgets/chat/missing_voice_button.dart create mode 100644 lib/pangea/widgets/chat/tts_controller.dart create mode 100644 lib/pangea/widgets/practice_activity/word_audio_button.dart create mode 100644 lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2eb411a23..68c90c59e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -158,4 +158,10 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 83de9e423..4e709a9c2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4239,6 +4239,10 @@ "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", + "playAudio": "Play", + "stop": "Stop", "grammarCopySCONJ": "Subordinating Conjunction", "grammarCopyNUM": "Number", "grammarCopyVERB": "Verb", diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 3213d085f..66417d921 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -25,7 +25,13 @@ class AudioPlayerWidget extends StatefulWidget { static String? currentId; - static const int wavesCount = 40; + // #Pangea + // static const int wavesCount = 40; + static const int wavesCount = kIsWeb ? 100 : 40; + + final int? sectionStartMS; + final int? sectionEndMS; + // Pangea# const AudioPlayerWidget( this.event, { @@ -33,6 +39,8 @@ class AudioPlayerWidget extends StatefulWidget { // #Pangea this.matrixFile, this.autoplay = false, + this.sectionStartMS, + this.sectionEndMS, // Pangea# super.key, }); @@ -72,6 +80,24 @@ class AudioPlayerState extends State { super.dispose(); } + // #Pangea + // @override + // void didUpdateWidget(covariant oldWidget) { + // if ((oldWidget.sectionEndMS != widget.sectionEndMS) || + // (oldWidget.sectionStartMS != widget.sectionStartMS)) { + // debugPrint('selection changed'); + // if (widget.sectionStartMS != null) { + // audioPlayer?.seek(Duration(milliseconds: widget.sectionStartMS!)); + // audioPlayer?.play(); + // } else { + // audioPlayer?.stop(); + // audioPlayer?.seek(null); + // } + // } + // super.didUpdateWidget(oldWidget); + // } + // Pangea# + Future _downloadAction() async { // #Pangea // if (status != AudioPlayerStatus.notDownloaded) return; @@ -160,7 +186,16 @@ class AudioPlayerState extends State { AudioPlayerWidget.wavesCount) .round(); }); + // #Pangea + // if (widget.sectionStartMS != null && + // widget.sectionEndMS != null && + // state.inMilliseconds.toDouble() >= widget.sectionEndMS!) { + // audioPlayer.stop(); + // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!)); + // } else if (state.inMilliseconds.toDouble() == maxPosition) { + // if (state.inMilliseconds.toDouble() == maxPosition) { + // Pangea# audioPlayer.stop(); audioPlayer.seek(null); } @@ -194,6 +229,11 @@ class AudioPlayerState extends State { } // Pangea# } + // #Pangea + // if (widget.sectionStartMS != null) { + // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!)); + // } + // Pangea# audioPlayer.play().onError( ErrorReporter(context, 'Unable to play audio message') .onErrorCallback, @@ -311,6 +351,17 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; + + // #Pangea + final msPerWave = (maxPosition / AudioPlayerWidget.wavesCount); + final int? startWave = widget.sectionStartMS != null && msPerWave > 0 + ? (widget.sectionStartMS! / msPerWave).floor() + : null; + final int? endWave = widget.sectionEndMS != null && msPerWave > 0 + ? (widget.sectionEndMS! / msPerWave).ceil() + : null; + // Pangea# + return Padding( // #Pangea // padding: const EdgeInsets.all(12.0), @@ -352,44 +403,101 @@ class AudioPlayerState extends State { // #Pangea // const SizedBox(width: 8), const SizedBox(width: 5), - // Pangea# - Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) - GestureDetector( - onTapDown: (_) => audioPlayer?.seek( - Duration( - milliseconds: - (maxPosition / AudioPlayerWidget.wavesCount).round() * - i, - ), - ), - child: Container( - height: 32, - color: widget.color.withAlpha(0), - alignment: Alignment.center, - child: Opacity( - opacity: currentPosition > i ? 1 : 0.5, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.color, - borderRadius: BorderRadius.circular(2), + // Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) + // GestureDetector( + // onTapDown: (_) => audioPlayer?.seek( + // Duration( + // milliseconds: + // (maxPosition / AudioPlayerWidget.wavesCount).round() * + // i, + // ), + // ), + // child: Container( + // height: 32, + // color: widget.color.withAlpha(0), + // alignment: Alignment.center, + // child: Opacity( + // opacity: currentPosition > i ? 1 : 0.5, + // child: Container( + // margin: const EdgeInsets.symmetric(horizontal: 1), + // decoration: BoxDecoration( + // color: widget.color, + // borderRadius: BorderRadius.circular(2), + // ), + // // #Pangea + // // width: 2, + // width: 1, + // // Pangea# + // height: 32 * (waveform[i] / 1024), + // ), + // ), + // ), + // ), + // ], + // ), + // const SizedBox(width: 8), + Expanded( + child: Row( + children: [ + for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) + Builder( + builder: (context) { + final double barOpacity = currentPosition > i ? 1 : 0.5; + return Expanded( + child: GestureDetector( + onTapDown: (_) { + audioPlayer?.seek( + Duration( + milliseconds: + (maxPosition / AudioPlayerWidget.wavesCount) + .round() * + i, + ), + ); + }, + child: Stack( + children: [ + Container( + margin: const EdgeInsets.symmetric( + horizontal: 0.5, + ), + decoration: BoxDecoration( + color: widget.color.withOpacity(barOpacity), + borderRadius: BorderRadius.circular(2), + ), + height: 32 * (waveform[i] / 1024), + ), + ], + ), ), - // #Pangea - // width: 2, - width: 1, - // Pangea# - height: 32 * (waveform[i] / 1024), - ), - ), + ); + // return Container( + // height: 32, + // width: 2, + // alignment: Alignment.center, + // child: Opacity( + // opacity: barOpacity, + // child: Container( + // margin: const EdgeInsets.symmetric( + // horizontal: 1, + // ), + // decoration: BoxDecoration( + // color: widget.color, + // borderRadius: BorderRadius.circular(2), + // ), + // height: 32 * (waveform[i] / 1024), + // width: 2, + // ), + // ), + // ); + }, ), - ), - ], + ], + ), ), - // #Pangea - // const SizedBox(width: 8), const SizedBox(width: 5), // SizedBox( // width: 36, diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 1d3c7f7ae..a8d7cca36 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -162,7 +162,7 @@ class PracticeGenerationController { activityType: ActivityTypeEnum.multipleChoice, langCode: event.messageDisplayLangCode, msgId: event.eventId, - multipleChoice: MultipleChoice( + content: ActivityContent( question: "What is a synonym for 'happy'?", choices: ["sad", "angry", "joyful", "tired"], answer: "joyful", diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index 069722590..e032c4045 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -5,20 +5,93 @@ import 'dart:typed_data'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:http/http.dart'; import '../network/requests.dart'; +class PangeaAudioEventData { + final String text; + final String langCode; + final List tokens; + + PangeaAudioEventData({ + required this.text, + required this.langCode, + required this.tokens, + }); + + factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData( + text: json[ModelKey.text] as String, + langCode: json[ModelKey.langCode] as String, + tokens: List.from( + (json[ModelKey.tokens] as Iterable) + .map((x) => TTSToken.fromJson(x)) + .toList(), + ), + ); + + Map toJson() => { + ModelKey.text: text, + ModelKey.langCode: langCode, + ModelKey.tokens: + List>.from(tokens.map((x) => x.toJson())), + }; +} + +class TTSToken { + final int startMS; + final int endMS; + final PangeaTokenText text; + + TTSToken({ + required this.startMS, + required this.endMS, + required this.text, + }); + + factory TTSToken.fromJson(Map json) => TTSToken( + startMS: json["start_ms"], + endMS: json["end_ms"], + text: PangeaTokenText.fromJson(json["text"]), + ); + + Map toJson() => { + "start_ms": startMS, + "end_ms": endMS, + "text": text.toJson(), + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TTSToken && + other.startMS == startMS && + other.endMS == endMS && + other.text == text; + } + + @override + int get hashCode => startMS.hashCode ^ endMS.hashCode ^ text.hashCode; +} + class TextToSpeechRequest { String text; String langCode; + List tokens; - TextToSpeechRequest({required this.text, required this.langCode}); + TextToSpeechRequest({ + required this.text, + required this.langCode, + required this.tokens, + }); Map toJson() => { ModelKey.text: text, ModelKey.langCode: langCode, + ModelKey.tokens: tokens.map((token) => token.toJson()).toList(), }; @override @@ -40,6 +113,7 @@ class TextToSpeechResponse { int durationMillis; List waveform; String fileExtension; + List ttsTokens; TextToSpeechResponse({ required this.audioContent, @@ -47,6 +121,7 @@ class TextToSpeechResponse { required this.durationMillis, required this.waveform, required this.fileExtension, + required this.ttsTokens, }); factory TextToSpeechResponse.fromJson( @@ -58,7 +133,27 @@ class TextToSpeechResponse { durationMillis: json["duration_millis"], waveform: List.from(json["wave_form"]), fileExtension: json["file_extension"], + ttsTokens: List.from( + json["tts_tokens"].map((x) => TTSToken.fromJson(x)), + ), ); + + Map toJson() => { + "audio_content": audioContent, + "mime_type": mimeType, + "duration_millis": durationMillis, + "wave_form": List.from(waveform.map((x) => x)), + "file_extension": fileExtension, + "tts_tokens": List.from(ttsTokens.map((x) => x.toJson())), + }; + + PangeaAudioEventData toPangeaAudioEventData(String text, String langCode) { + return PangeaAudioEventData( + text: text, + langCode: langCode, + tokens: ttsTokens, + ); + } } class _TextToSpeechCacheItem { diff --git a/lib/pangea/enum/activity_display_instructions_enum.dart b/lib/pangea/enum/activity_display_instructions_enum.dart index 9a96d669c..36dc530b5 100644 --- a/lib/pangea/enum/activity_display_instructions_enum.dart +++ b/lib/pangea/enum/activity_display_instructions_enum.dart @@ -1,13 +1,6 @@ -enum ActivityDisplayInstructionsEnum { highlight, hide } +enum ActivityDisplayInstructionsEnum { highlight, hide, nothing } extension ActivityDisplayInstructionsEnumExt on ActivityDisplayInstructionsEnum { - String get string { - switch (this) { - case ActivityDisplayInstructionsEnum.highlight: - return 'highlight'; - case ActivityDisplayInstructionsEnum.hide: - return 'hide'; - } - } + String get string => toString().split('.').last; } diff --git a/lib/pangea/enum/activity_type_enum.dart b/lib/pangea/enum/activity_type_enum.dart index d429aa038..eace349d2 100644 --- a/lib/pangea/enum/activity_type_enum.dart +++ b/lib/pangea/enum/activity_type_enum.dart @@ -1,4 +1,10 @@ -enum ActivityTypeEnum { multipleChoice, freeResponse, listening, speaking } +enum ActivityTypeEnum { + multipleChoice, + freeResponse, + listening, + speaking, + wordFocusListening +} extension ActivityTypeExtension on ActivityTypeEnum { String get string { @@ -11,6 +17,8 @@ extension ActivityTypeExtension on ActivityTypeEnum { return 'listening'; case ActivityTypeEnum.speaking: return 'speaking'; + case ActivityTypeEnum.wordFocusListening: + return 'word_focus_listening'; } } } diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index ab953d24d..1f1d37dfe 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -38,63 +38,49 @@ enum ConstructUseTypeEnum { /// was target construct in word meaning in context practice activity and incorrectly selected incPA, + + /// was target lemma in word-focus listening activity and correctly selected + corWL, + + /// form of lemma was read-aloud in word-focus listening activity and incorrectly selected + incWL, + + /// form of lemma was read-aloud in word-focus listening activity and correctly ignored + ignWL, + + /// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client + nan } extension ConstructUseTypeExtension on ConstructUseTypeEnum { - String get string { - switch (this) { - case ConstructUseTypeEnum.ga: - return 'ga'; - case ConstructUseTypeEnum.wa: - return 'wa'; - case ConstructUseTypeEnum.corIt: - return 'corIt'; - case ConstructUseTypeEnum.incIt: - return 'incIt'; - case ConstructUseTypeEnum.ignIt: - return 'ignIt'; - case ConstructUseTypeEnum.ignIGC: - return 'ignIGC'; - case ConstructUseTypeEnum.corIGC: - return 'corIGC'; - case ConstructUseTypeEnum.incIGC: - return 'incIGC'; - case ConstructUseTypeEnum.unk: - return 'unk'; - case ConstructUseTypeEnum.corPA: - return 'corPA'; - case ConstructUseTypeEnum.incPA: - return 'incPA'; - case ConstructUseTypeEnum.ignPA: - return 'ignPA'; - } - } + String get string => toString().split('.').last; IconData get icon { switch (this) { - case ConstructUseTypeEnum.ga: - return Icons.check; case ConstructUseTypeEnum.wa: return Icons.thumb_up_sharp; + case ConstructUseTypeEnum.corIt: - return Icons.translate; case ConstructUseTypeEnum.incIt: - return Icons.translate; case ConstructUseTypeEnum.ignIt: return Icons.translate; + case ConstructUseTypeEnum.ignIGC: - return Icons.close; - case ConstructUseTypeEnum.corIGC: - return Icons.check; case ConstructUseTypeEnum.incIGC: - return Icons.close; - case ConstructUseTypeEnum.corPA: - return Icons.check; case ConstructUseTypeEnum.incPA: - return Icons.close; case ConstructUseTypeEnum.ignPA: + case ConstructUseTypeEnum.ignWL: + case ConstructUseTypeEnum.incWL: return Icons.close; + + case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.corIGC: + case ConstructUseTypeEnum.corPA: + case ConstructUseTypeEnum.corWL: + return Icons.check; + case ConstructUseTypeEnum.unk: + case ConstructUseTypeEnum.nan: return Icons.help; } } @@ -107,30 +93,35 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { /// Practice activities get a moderate amount of points. int get pointValue { switch (this) { - case ConstructUseTypeEnum.ga: - return 2; + case ConstructUseTypeEnum.corPA: + return 5; + case ConstructUseTypeEnum.wa: + case ConstructUseTypeEnum.corWL: return 3; + + case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.corIGC: + return 2; + case ConstructUseTypeEnum.corIt: - return 1; - case ConstructUseTypeEnum.incIt: - return -1; case ConstructUseTypeEnum.ignIt: - return 1; case ConstructUseTypeEnum.ignIGC: + case ConstructUseTypeEnum.ignPA: + case ConstructUseTypeEnum.ignWL: return 1; - case ConstructUseTypeEnum.corIGC: - return 2; - case ConstructUseTypeEnum.incIGC: - return -1; + case ConstructUseTypeEnum.unk: + case ConstructUseTypeEnum.nan: return 0; - case ConstructUseTypeEnum.corPA: - return 5; + + case ConstructUseTypeEnum.incIt: + case ConstructUseTypeEnum.incIGC: + return -1; + case ConstructUseTypeEnum.incPA: + case ConstructUseTypeEnum.incWL: return -2; - case ConstructUseTypeEnum.ignPA: - return 1; } } } diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index f18ee23b7..23a0c1374 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -1,11 +1,15 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -37,4 +41,42 @@ extension PangeaEvent on Event { throw Exception("$type events do not have pangea content"); } } + + Future getPangeaAudioFile() async { + if (type != EventTypes.Message || messageType != MessageTypes.Audio) { + ErrorHandler.logError( + e: "Event $eventId is not an audio message", + ); + return null; + } + + // @ggurdin what are cases where these would be null? + // if it would be unexpected, we should log an error with details to investigate + final transcription = + content.tryGetMap(ModelKey.transcription); + final audioContent = + content.tryGetMap('org.matrix.msc1767.audio'); + if (transcription == null || audioContent == null) return null; + + final matrixFile = await downloadAndDecryptAttachment(); + final duration = audioContent.tryGet('duration'); + final waveform = audioContent.tryGetList('waveform'); + + // old audio messages will not have tokens + final tokensContent = transcription.tryGetList(ModelKey.tokens); + if (tokensContent == null) return null; + + final tokens = tokensContent + .map((e) => TTSToken.fromJson(e as Map)) + .toList(); + + return PangeaAudioFile( + bytes: matrixFile.bytes, + name: matrixFile.name, + tokens: tokens, + mimeType: matrixFile.mimeType, + duration: duration, + waveform: waveform, + ); + } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 5e5a4a059..13da5ace8 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -81,17 +81,17 @@ class PangeaMessageEvent { _representations = null; } - Future getMatrixAudioFile( + Future getMatrixAudioFile( String langCode, BuildContext context, ) async { - final String text = (await representationByLanguageGlobal( - langCode: langCode, - )) - ?.text ?? - body; + final RepresentationEvent? rep = representationByLanguage(langCode); + + if (rep == null) return null; + final TextToSpeechRequest params = TextToSpeechRequest( - text: text, + text: rep.content.text, + tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(), langCode: langCode, ); @@ -111,9 +111,10 @@ class PangeaMessageEvent { mimeType: response.mimeType, duration: response.durationMillis, waveform: response.waveform, + tokens: response.ttsTokens, ); - sendAudioEvent(file, response, text, langCode); + sendAudioEvent(file, response, rep.text, langCode); return file; } @@ -137,10 +138,8 @@ class PangeaMessageEvent { 'duration': response.durationMillis, 'waveform': response.waveform, }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, + ModelKey.transcription: + response.toPangeaAudioEventData(text, langCode).toJson(), }, ); @@ -155,97 +154,46 @@ class PangeaMessageEvent { return audioEvent; } - //get audio for text and language - //if no audio exists, create it - //if audio exists, return it - Future getTextToSpeechGlobal(String langCode) async { - final String text = representationByLanguage(langCode)?.text ?? body; - - final local = getTextToSpeechLocal(langCode, text); - - if (local != null) return Future.value(local); - - final TextToSpeechRequest params = TextToSpeechRequest( - text: text, - langCode: langCode, - ); - - final TextToSpeechResponse response = - await MatrixState.pangeaController.textToSpeech.get( - params, - ); - - final audioBytes = base64.decode(response.audioContent); - - // if (!TextToSpeechController.isOggFile(audioBytes)) { - // throw Exception("File is not a valid OGG format"); - // } else { - // debugPrint("File is a valid OGG format"); - // } - - // from text, trim whitespace, remove special characters, and limit to 20 characters - // final fileName = - // text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20); - final eventIdParam = _event.eventId; - final fileName = - "audio_for_${eventIdParam}_$langCode.${response.fileExtension}"; - - final file = MatrixAudioFile( - bytes: audioBytes, - name: fileName, - mimeType: response.mimeType, - ); - - // try { - final String? eventId = await room.sendFileEvent( - file, - inReplyTo: _event, - extraContent: { - 'info': { - ...file.info, - 'duration': response.durationMillis, - }, - 'org.matrix.msc3245.voice': {}, - 'org.matrix.msc1767.audio': { - 'duration': response.durationMillis, - 'waveform': response.waveform, - }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, - }, - ); - // .timeout( - // Durations.long4, - // onTimeout: () { - // debugPrint("timeout in getTextToSpeechGlobal"); - // return null; - // }, - // ); - - debugPrint("eventId in getTextToSpeechGlobal $eventId"); - return eventId != null ? room.getEventById(eventId) : null; - } - Event? getTextToSpeechLocal(String langCode, String text) { return allAudio.firstWhereOrNull( - (element) { - // Safely access the transcription map - final transcription = element.content.tryGetMap(ModelKey.transcription); - - // return transcription != null; - if (transcription == null) { - // If transcription is null, this element does not match. + (event) { + try { + // Safely access + final dataMap = event.content.tryGetMap(ModelKey.transcription); + + if (dataMap == null) { + return false; + } + + // old text to speech content will not have TTSToken data + // we want to disregard them and just generate new ones + // for that, we'll return false if 'tokens' are null + // while in-development, we'll pause here to inspect + // debugger can be removed after we're sure it's working + if (dataMap['tokens'] == null) { + // events before today will definitely not have the tokens + debugger( + when: kDebugMode && + event.originServerTs.isAfter(DateTime(2024, 10, 16)), + ); + return false; + } + + final PangeaAudioEventData audioData = + PangeaAudioEventData.fromJson(dataMap as dynamic); + + // Check if both language code and text match + return audioData.langCode == langCode && audioData.text == text; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: e, + s: s, + data: event.content.tryGetMap(ModelKey.transcription), + m: "error parsing data in getTextToSpeechLocal", + ); return false; } - - // Safely get language code and text from the transcription - final elementLangCode = transcription[ModelKey.langCode]; - final elementText = transcription[ModelKey.text]; - - // Check if both language code and text matsch - return elementLangCode == langCode && elementText == text; }, ); } diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 2960d7b1d..b9cd02cd8 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,195 +1,195 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import '../enum/vocab_proficiency_enum.dart'; - -class VocabHeadwords { - List lists; - - VocabHeadwords({ - required this.lists, - }); - - /// in json parameter, keys are the names of the VocabList - /// values are the words in the VocabList - factory VocabHeadwords.fromJson(Map json) { - final List lists = []; - for (final entry in json.entries) { - lists.add( - VocabList( - name: entry.key, - lemmas: (entry.value as Iterable).cast().toList(), - ), - ); - } - return VocabHeadwords(lists: lists); - } - - static Future getHeadwords(String langCode) async { - final String data = - await rootBundle.loadString('${langCode}_headwords.json'); - final decoded = jsonDecode(data); - final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded); - return headwords; - } -} - -class VocabList { - String name; - - /// key is lemma - Map words = {}; - - VocabList({ - required this.name, - required List lemmas, - }) { - for (final lemma in lemmas) { - words[lemma] = VocabTotals.newTotals; - } - } - - void addVocabUse(String lemma, List use) { - words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use); - } - - ListTotals calculuateTotals() { - final ListTotals listTotals = ListTotals.empty; - for (final word in words.entries) { - debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase()); - listTotals.addByType(word.value.proficiencyLevel); - } - return listTotals; - } -} - -class ListTotals { - int low; - int medium; - int high; - int unknown; - - ListTotals({ - required this.low, - required this.medium, - required this.high, - required this.unknown, - }); - - static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0); - - void addByType(VocabProficiencyEnum prof) { - switch (prof) { - case VocabProficiencyEnum.low: - low++; - break; - case VocabProficiencyEnum.medium: - medium++; - break; - case VocabProficiencyEnum.high: - high++; - break; - case VocabProficiencyEnum.unk: - unknown++; - break; - } - } -} - -class VocabTotals { - num ga; - - num wa; - - num corIt; - - num incIt; - - num ignIt; - - VocabTotals({ - required this.ga, - required this.wa, - required this.corIt, - required this.incIt, - required this.ignIt, - }); - - num get calculateEstimatedVocabProficiency { - const num gaWeight = -1; - const num waWeight = 1; - const num corItWeight = 0.5; - const num incItWeight = -0.5; - const num ignItWeight = 0.1; - - final num gaScore = ga * gaWeight; - final num waScore = wa * waWeight; - final num corItScore = corIt * corItWeight; - final num incItScore = incIt * incItWeight; - final num ignItScore = ignIt * ignItWeight; - - final num totalScore = - gaScore + waScore + corItScore + incItScore + ignItScore; - - return totalScore; - } - - VocabProficiencyEnum get proficiencyLevel => - VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency); - - static VocabTotals get newTotals { - return VocabTotals( - ga: 0, - wa: 0, - corIt: 0, - incIt: 0, - ignIt: 0, - ); - } - - void addVocabUseBasedOnUseType(List uses) { - for (final use in uses) { - switch (use.useType) { - case ConstructUseTypeEnum.ga: - ga++; - break; - case ConstructUseTypeEnum.wa: - wa++; - break; - case ConstructUseTypeEnum.corIt: - corIt++; - break; - case ConstructUseTypeEnum.incIt: - incIt++; - break; - case ConstructUseTypeEnum.ignIt: - ignIt++; - break; - //TODO - these shouldn't be counted as such - case ConstructUseTypeEnum.ignIGC: - ignIt++; - break; - case ConstructUseTypeEnum.corIGC: - corIt++; - break; - case ConstructUseTypeEnum.incIGC: - incIt++; - break; - //TODO if we bring back Headwords then we need to add these - case ConstructUseTypeEnum.corPA: - break; - case ConstructUseTypeEnum.incPA: - break; - case ConstructUseTypeEnum.unk: - break; - case ConstructUseTypeEnum.ignPA: - break; - } - } - } -} +// import 'dart:convert'; +// import 'dart:developer'; + +// import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +// import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/services.dart'; + +// import '../enum/vocab_proficiency_enum.dart'; + +// class VocabHeadwords { +// List lists; + +// VocabHeadwords({ +// required this.lists, +// }); + +// /// in json parameter, keys are the names of the VocabList +// /// values are the words in the VocabList +// factory VocabHeadwords.fromJson(Map json) { +// final List lists = []; +// for (final entry in json.entries) { +// lists.add( +// VocabList( +// name: entry.key, +// lemmas: (entry.value as Iterable).cast().toList(), +// ), +// ); +// } +// return VocabHeadwords(lists: lists); +// } + +// static Future getHeadwords(String langCode) async { +// final String data = +// await rootBundle.loadString('${langCode}_headwords.json'); +// final decoded = jsonDecode(data); +// final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded); +// return headwords; +// } +// } + +// class VocabList { +// String name; + +// /// key is lemma +// Map words = {}; + +// VocabList({ +// required this.name, +// required List lemmas, +// }) { +// for (final lemma in lemmas) { +// words[lemma] = VocabTotals.newTotals; +// } +// } + +// void addVocabUse(String lemma, List use) { +// words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use); +// } + +// ListTotals calculuateTotals() { +// final ListTotals listTotals = ListTotals.empty; +// for (final word in words.entries) { +// debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase()); +// listTotals.addByType(word.value.proficiencyLevel); +// } +// return listTotals; +// } +// } + +// class ListTotals { +// int low; +// int medium; +// int high; +// int unknown; + +// ListTotals({ +// required this.low, +// required this.medium, +// required this.high, +// required this.unknown, +// }); + +// static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0); + +// void addByType(VocabProficiencyEnum prof) { +// switch (prof) { +// case VocabProficiencyEnum.low: +// low++; +// break; +// case VocabProficiencyEnum.medium: +// medium++; +// break; +// case VocabProficiencyEnum.high: +// high++; +// break; +// case VocabProficiencyEnum.unk: +// unknown++; +// break; +// } +// } +// } + +// class VocabTotals { +// num ga; + +// num wa; + +// num corIt; + +// num incIt; + +// num ignIt; + +// VocabTotals({ +// required this.ga, +// required this.wa, +// required this.corIt, +// required this.incIt, +// required this.ignIt, +// }); + +// num get calculateEstimatedVocabProficiency { +// const num gaWeight = -1; +// const num waWeight = 1; +// const num corItWeight = 0.5; +// const num incItWeight = -0.5; +// const num ignItWeight = 0.1; + +// final num gaScore = ga * gaWeight; +// final num waScore = wa * waWeight; +// final num corItScore = corIt * corItWeight; +// final num incItScore = incIt * incItWeight; +// final num ignItScore = ignIt * ignItWeight; + +// final num totalScore = +// gaScore + waScore + corItScore + incItScore + ignItScore; + +// return totalScore; +// } + +// VocabProficiencyEnum get proficiencyLevel => +// VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency); + +// static VocabTotals get newTotals { +// return VocabTotals( +// ga: 0, +// wa: 0, +// corIt: 0, +// incIt: 0, +// ignIt: 0, +// ); +// } + +// void addVocabUseBasedOnUseType(List uses) { +// for (final use in uses) { +// switch (use.useType) { +// case ConstructUseTypeEnum.ga: +// ga++; +// break; +// case ConstructUseTypeEnum.wa: +// wa++; +// break; +// case ConstructUseTypeEnum.corIt: +// corIt++; +// break; +// case ConstructUseTypeEnum.incIt: +// incIt++; +// break; +// case ConstructUseTypeEnum.ignIt: +// ignIt++; +// break; +// //TODO - these shouldn't be counted as such +// case ConstructUseTypeEnum.ignIGC: +// ignIt++; +// break; +// case ConstructUseTypeEnum.corIGC: +// corIt++; +// break; +// case ConstructUseTypeEnum.incIGC: +// incIt++; +// break; +// //TODO if we bring back Headwords then we need to add these +// case ConstructUseTypeEnum.corPA: +// break; +// case ConstructUseTypeEnum.incPA: +// break; +// case ConstructUseTypeEnum.unk: +// break; +// case ConstructUseTypeEnum.ignPA: +// break; +// } +// } +// } +// } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index e6b577c20..27361f272 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -128,8 +128,6 @@ class PangeaToken { lemma: lemma.text, type: ConstructTypeEnum.vocab, ), - xp: 0, - lastUsed: null, ), ); @@ -140,8 +138,6 @@ class PangeaToken { lemma: morph.key, type: ConstructTypeEnum.morph, ), - xp: 0, - lastUsed: null, ), ); } 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 458619d20..6261a0215 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; @@ -7,11 +8,13 @@ class ConstructWithXP { final ConstructIdentifier id; int xp; DateTime? lastUsed; + List condensedConstructUses; ConstructWithXP({ required this.id, - required this.xp, - required this.lastUsed, + this.xp = 0, + this.lastUsed, + this.condensedConstructUses = const [], }); factory ConstructWithXP.fromJson(Map json) { @@ -23,6 +26,14 @@ class ConstructWithXP { lastUsed: json['last_used'] != null ? DateTime.parse(json['last_used'] as String) : null, + condensedConstructUses: (json['uses'] as List).map((e) { + return ConstructUseTypeEnum.values.firstWhereOrNull( + (element) => + element.string == e || + element.toString().split('.').last == e, + ) ?? + ConstructUseTypeEnum.nan; + }).toList(), ); } @@ -31,6 +42,7 @@ class ConstructWithXP { 'construct_id': id.toJson(), 'xp': xp, 'last_used': lastUsed?.toIso8601String(), + 'uses': condensedConstructUses.map((e) => e.string).toList(), }; return json; } diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index 28c18d7c0..9c6468db9 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -5,13 +5,13 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class MultipleChoice { +class ActivityContent { final String question; final List choices; final String answer; final RelevantSpanDisplayDetails? spanDisplayDetails; - MultipleChoice({ + ActivityContent({ required this.question, required this.choices, required this.answer, @@ -37,12 +37,12 @@ class MultipleChoice { Color choiceColor(int index) => index == correctAnswerIndex ? AppConfig.success : AppConfig.warning; - factory MultipleChoice.fromJson(Map json) { + factory ActivityContent.fromJson(Map json) { final spanDisplay = json['span_display_details'] != null && json['span_display_details'] is Map ? RelevantSpanDisplayDetails.fromJson(json['span_display_details']) : null; - return MultipleChoice( + return ActivityContent( question: json['question'] as String, choices: (json['choices'] as List).map((e) => e as String).toList(), answer: json['answer'] ?? json['correct_answer'] as String, diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 7c02a7aae..55b171397 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -165,110 +165,30 @@ class PracticeActivityRequest { } } -class FreeResponse { - final String question; - final String correctAnswer; - final String gradingGuide; - - FreeResponse({ - required this.question, - required this.correctAnswer, - required this.gradingGuide, - }); - - factory FreeResponse.fromJson(Map json) { - return FreeResponse( - question: json['question'] as String, - correctAnswer: json['correct_answer'] as String, - gradingGuide: json['grading_guide'] as String, - ); - } - - Map toJson() { - return { - 'question': question, - 'correct_answer': correctAnswer, - 'grading_guide': gradingGuide, - }; - } -} - -class Listening { - final String audioUrl; - final String text; - - Listening({required this.audioUrl, required this.text}); - - factory Listening.fromJson(Map json) { - return Listening( - audioUrl: json['audio_url'] as String, - text: json['text'] as String, - ); - } - - Map toJson() { - return { - 'audio_url': audioUrl, - 'text': text, - }; - } -} - -class Speaking { - final String text; - - Speaking({required this.text}); - - factory Speaking.fromJson(Map json) { - return Speaking( - text: json['text'] as String, - ); - } - - Map toJson() { - return { - 'text': text, - }; - } -} - class PracticeActivityModel { final List tgtConstructs; final String langCode; final String msgId; final ActivityTypeEnum activityType; - final MultipleChoice? multipleChoice; - final Listening? listening; - final Speaking? speaking; - final FreeResponse? freeResponse; + final ActivityContent content; PracticeActivityModel({ required this.tgtConstructs, required this.langCode, required this.msgId, required this.activityType, - this.multipleChoice, - this.listening, - this.speaking, - this.freeResponse, + required this.content, }); - String get question { - switch (activityType) { - case ActivityTypeEnum.multipleChoice: - return multipleChoice!.question; - case ActivityTypeEnum.listening: - return listening!.text; - case ActivityTypeEnum.speaking: - return speaking!.text; - case ActivityTypeEnum.freeResponse: - return freeResponse!.question; - default: - return ''; - } - } + String get question => content.question; factory PracticeActivityModel.fromJson(Map json) { + // moving from multiple_choice to content as the key + // this is to make the model more generic + // here for backward compatibility + final Map content = + (json['content'] ?? json["multiple_choice"]) as Map; + return PracticeActivityModel( tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) as List) @@ -283,27 +203,14 @@ class PracticeActivityModel { e.string == json['activity_type'] as String || e.string.split('.').last == json['activity_type'] as String, ), - multipleChoice: json['multiple_choice'] != null - ? MultipleChoice.fromJson( - json['multiple_choice'] as Map, - ) - : null, - listening: json['listening'] != null - ? Listening.fromJson(json['listening'] as Map) - : null, - speaking: json['speaking'] != null - ? Speaking.fromJson(json['speaking'] as Map) - : null, - freeResponse: json['free_response'] != null - ? FreeResponse.fromJson( - json['free_response'] as Map, - ) - : null, + content: ActivityContent.fromJson( + content, + ), ); } RelevantSpanDisplayDetails? get relevantSpanDisplayDetails => - multipleChoice?.spanDisplayDetails; + content.spanDisplayDetails; Map toJson() { return { @@ -311,10 +218,7 @@ class PracticeActivityModel { 'lang_code': langCode, 'msg_id': msgId, 'activity_type': activityType.string, - 'multiple_choice': multipleChoice?.toJson(), - 'listening': listening?.toJson(), - 'speaking': speaking?.toJson(), - 'free_response': freeResponse?.toJson(), + 'content': content.toJson(), }; } @@ -328,10 +232,7 @@ class PracticeActivityModel { other.langCode == langCode && other.msgId == msgId && other.activityType == activityType && - other.multipleChoice == multipleChoice && - other.listening == listening && - other.speaking == speaking && - other.freeResponse == freeResponse; + other.content == content; } @override @@ -340,10 +241,7 @@ class PracticeActivityModel { langCode.hashCode ^ msgId.hashCode ^ activityType.hashCode ^ - multipleChoice.hashCode ^ - listening.hashCode ^ - speaking.hashCode ^ - freeResponse.hashCode; + content.hashCode; } } @@ -372,7 +270,7 @@ class RelevantSpanDisplayDetails { return RelevantSpanDisplayDetails( offset: json['offset'] as int, length: json['length'] as int, - displayInstructions: display ?? ActivityDisplayInstructionsEnum.hide, + displayInstructions: display ?? ActivityDisplayInstructionsEnum.nothing, ); } @@ -384,7 +282,6 @@ class RelevantSpanDisplayDetails { }; } - // override operator == and hashCode @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index b190da291..b56e7103e 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -1,10 +1,18 @@ +import 'dart:developer'; +import 'dart:math'; + import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -12,11 +20,13 @@ import 'package:matrix/matrix.dart'; class MessageAudioCard extends StatefulWidget { final PangeaMessageEvent messageEvent; final MessageOverlayController overlayController; + final PangeaTokenText? selection; const MessageAudioCard({ super.key, required this.messageEvent, required this.overlayController, + this.selection, }); @override @@ -25,9 +35,113 @@ class MessageAudioCard extends StatefulWidget { class MessageAudioCardState extends State { bool _isLoading = false; - Event? localAudioEvent; PangeaAudioFile? audioFile; + int? sectionStartMS; + int? sectionEndMS; + + TtsController tts = TtsController(); + + @override + void initState() { + super.initState(); + fetchAudio(); + + // initializeTTS(); + } + + // initializeTTS() async { + // tts.setupTTS().then((value) => setState(() {})); + // } + + @override + void didUpdateWidget(covariant oldWidget) { + // @ggurdin did you find a case of needing to reinitialize TTS because of a language change? + // if (widget.messageEvent.messageDisplayLangCode != + // oldWidget.messageEvent.messageDisplayLangCode) { + // initializeTTS(); + // } + + if (oldWidget.selection != widget.selection) { + debugPrint('selection changed'); + setSectionStartAndEndFromSelection(); + playSelectionAudio(); + } + super.didUpdateWidget(oldWidget); + } + + Future playSelectionAudio() async { + final PangeaTokenText selection = widget.selection!; + final tokenText = selection.content; + + await tts.speak(tokenText); + } + + void setSectionStartAndEnd(int? start, int? end) => mounted + ? setState(() { + sectionStartMS = start; + sectionEndMS = end; + }) + : null; + + void setSectionStartAndEndFromSelection() async { + if (audioFile == null) { + // should never happen but just in case + debugger(when: kDebugMode); + return; + } + + if (audioFile!.duration == null) { + // should never happen but just in case + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception(), + m: 'audioFile duration is null in MessageAudioCardState', + data: { + 'audioFile': audioFile, + }, + ); + return setSectionStartAndEnd(null, null); + } + + // if there is no selection, we don't need to do anything + // but clear the section start and end + if (widget.selection == null) { + return setSectionStartAndEnd(null, null); + } + + final PangeaTokenText selection = widget.selection!; + final List tokens = audioFile!.tokens; + + // find the token that corresponds to the selection + // set the start to the start of the token + // set the end to the start of the next token or to the duration of the audio if + // if there is no next token + for (int i = 0; i < tokens.length; i++) { + final TTSToken ttsToken = tokens[i]; + if (ttsToken.text.offset == selection.offset) { + return setSectionStartAndEnd( + max(ttsToken.startMS - 150, 0), + min(ttsToken.endMS + 150, audioFile!.duration!), + ); + } + } + + // if we didn't find the token, we should pause if debug and log an error + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception(), + m: 'could not find token for selection in MessageAudioCardState', + data: { + 'selection': selection, + 'tokens': tokens, + 'sttTokens': audioFile!.tokens, + }, + ); + + setSectionStartAndEnd(null, null); + } + Future fetchAudio() async { if (!mounted) return; setState(() => _isLoading = true); @@ -36,20 +150,27 @@ class MessageAudioCardState extends State { final String langCode = widget.messageEvent.messageDisplayLangCode; final String? text = widget.messageEvent.representationByLanguage(langCode)?.text; - if (text != null) { - final Event? localEvent = - widget.messageEvent.getTextToSpeechLocal(langCode, text); - if (localEvent != null) { - localAudioEvent = localEvent; - if (mounted) setState(() => _isLoading = false); - return; - } + + if (text == null) { + //TODO - handle error but get out of flow } - audioFile = - await widget.messageEvent.getMatrixAudioFile(langCode, context); + final Event? localEvent = + widget.messageEvent.getTextToSpeechLocal(langCode, text!); + + if (localEvent != null) { + audioFile = await localEvent.getPangeaAudioFile(); + } else { + audioFile = await widget.messageEvent.getMatrixAudioFile( + langCode, + context, + ); + } + debugPrint("audio file is now: $audioFile. setting starts and ends..."); + setSectionStartAndEndFromSelection(); if (mounted) setState(() => _isLoading = false); } catch (e, s) { + debugger(when: kDebugMode); debugPrint(StackTrace.current.toString()); if (!mounted) return; setState(() => _isLoading = false); @@ -68,19 +189,6 @@ class MessageAudioCardState extends State { }, ); } - return; - } - - @override - void initState() { - super.initState(); - - //once we have audio for words, we'll play that - if (widget.overlayController.isSelection) { - widget.overlayController.clearSelection(); - } - - fetchAudio(); } @override @@ -91,15 +199,17 @@ class MessageAudioCardState extends State { alignment: Alignment.center, child: _isLoading ? const ToolbarContentLoadingIndicator() - : localAudioEvent != null || audioFile != null + : audioFile != null ? Column( children: [ AudioPlayerWidget( - localAudioEvent, - color: Theme.of(context).colorScheme.onPrimaryContainer, + null, matrixFile: audioFile, - autoplay: true, + sectionStartMS: sectionStartMS, + sectionEndMS: sectionEndMS, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), + tts.missingVoiceButton ?? const SizedBox(), ], ) : const CardErrorWidget(), @@ -109,6 +219,7 @@ class MessageAudioCardState extends State { class PangeaAudioFile extends MatrixAudioFile { List? waveform; + List tokens; PangeaAudioFile({ required super.bytes, @@ -116,5 +227,6 @@ class PangeaAudioFile extends MatrixAudioFile { super.mimeType, super.duration, this.waveform, + required this.tokens, }); } diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 428d36e79..532cd82e0 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.dart'; +import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.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/models/pangea_token_model.dart'; @@ -182,8 +183,10 @@ class MessageOverlayController extends State void onClickOverlayMessageToken( PangeaToken token, ) { - if ([MessageMode.practiceActivity, MessageMode.textToSpeech] - .contains(toolbarMode)) { + if ([ + MessageMode.practiceActivity, + // MessageMode.textToSpeech + ].contains(toolbarMode)) { return; } @@ -210,19 +213,23 @@ class MessageOverlayController extends State void setSelectedSpan(PracticeActivityModel activity) { final RelevantSpanDisplayDetails? span = - activity.multipleChoice?.spanDisplayDetails; + activity.content.spanDisplayDetails; if (span == null) { debugger(when: kDebugMode); return; } - _selectedSpan = PangeaTokenText( - offset: span.offset, - length: span.length, - content: widget._pangeaMessageEvent.messageDisplayText - .substring(span.offset, span.offset + span.length), - ); + if (span.displayInstructions != ActivityDisplayInstructionsEnum.nothing) { + _selectedSpan = PangeaTokenText( + offset: span.offset, + length: span.length, + content: widget._pangeaMessageEvent.messageDisplayText + .substring(span.offset, span.offset + span.length), + ); + } else { + _selectedSpan = null; + } setState(() {}); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 006b4e98d..102ecfc34 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -49,6 +49,7 @@ class MessageToolbar extends StatelessWidget { return MessageAudioCard( messageEvent: pangeaMessageEvent, overlayController: overLayController, + selection: overLayController.selectedSpan, ); case MessageMode.speechToText: return MessageSpeechToTextCard( diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index bd5b0802b..41cd47c6a 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -70,34 +70,32 @@ class ToolbarButtons extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: modes .mapIndexed( - (index, mode) => Tooltip( - message: mode.tooltip(context), - child: IconButton( - iconSize: 20, - icon: Icon(mode.icon), - color: mode == overlayController.toolbarMode - ? Colors.white - : null, - isSelected: mode == overlayController.toolbarMode, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - mode.iconButtonColor( - context, - index, - overlayController.toolbarMode, - pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, - ), + (index, mode) => IconButton( + iconSize: 20, + icon: Icon(mode.icon), + tooltip: mode.tooltip(context), + color: mode == overlayController.toolbarMode + ? Colors.white + : null, + isSelected: mode == overlayController.toolbarMode, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + mode.iconButtonColor( + context, + index, + overlayController.toolbarMode, + pangeaMessageEvent.numberOfActivitiesCompleted, + overlayController.isPracticeComplete, ), ), - onPressed: mode.isUnlocked( - index, - pangeaMessageEvent.numberOfActivitiesCompleted, - overlayController.isPracticeComplete, - ) - ? () => overlayController.updateToolbarMode(mode) - : null, ), + onPressed: mode.isUnlocked( + index, + pangeaMessageEvent.numberOfActivitiesCompleted, + overlayController.isPracticeComplete, + ) + ? () => overlayController.updateToolbarMode(mode) + : null, ), ) .toList(), diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart new file mode 100644 index 000000000..e1f8b74fb --- /dev/null +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:android_intent_plus/android_intent.dart'; +import 'package:fluffychat/config/app_config.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({ + required this.targetLangCode, + super.key, + }); + + void launchTTSSettings(BuildContext context) { + if (Platform.isAndroid) { + const intent = AndroidIntent( + action: 'com.android.settings.TTS_SETTINGS', + package: 'com.talktolearn.chat', + ); + + showFutureLoadingDialog( + context: context, + future: intent.launch, + ); + } + } + + @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), + ), + ), + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context)!.voiceNotAvailable, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: () => launchTTSSettings, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text(L10n.of(context)!.openVoiceSettings), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart new file mode 100644 index 000000000..e8edd65c3 --- /dev/null +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -0,0 +1,77 @@ +import 'dart:developer'; + +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'; +import 'package:flutter_tts/flutter_tts.dart' as flutter_tts; + +class TtsController { + String? targetLanguage; + + List availableLangCodes = []; + final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts(); + + // if targetLanguage isn't set here, it needs to be set later + TtsController() { + setupTTS(); + } + + Future setupTTS() async { + try { + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + debugger(when: kDebugMode && targetLanguage == null); + + debugPrint('setupTTS targetLanguage: $targetLanguage'); + + tts.setLanguage( + targetLanguage ?? "en", + ); + + await tts.awaitSpeakCompletion(true); + + final voices = await tts.getVoices; + availableLangCodes = (voices as List) + .map((v) { + // debugPrint('v: $v'); + + //@ggurdin i changed this from name to locale + //in my testing, that's where the language code is stored + // maybe it's different for different devices? was it different in your android testing? + // return v['name']?.split("-").first; + return v['locale']?.split("-").first; + }) + .toSet() + .cast() + .toList(); + + debugPrint("lang supported? $isLanguageFullySupported"); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } + } + + Future speak(String text) async { + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + await tts.stop(); + return tts.speak(text); + } + + bool get isLanguageFullySupported => + availableLangCodes.contains(targetLanguage); + + // @ggurdin + Widget get missingVoiceButton => targetLanguage != null && + (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) + ? const SizedBox.shrink() + : MissingVoiceButton( + targetLangCode: targetLanguage!, + ); +} diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 7675e39d3..5a1f50497 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -3,9 +3,11 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,7 +15,7 @@ import 'package:flutter/material.dart'; /// The multiple choice activity view class MultipleChoiceActivity extends StatefulWidget { final MessagePracticeActivityCardState practiceCardController; - final PracticeActivityModel? currentActivity; + final PracticeActivityModel currentActivity; const MultipleChoiceActivity({ super.key, @@ -52,7 +54,7 @@ class MultipleChoiceActivityState extends State { } final bool isCorrect = - widget.currentActivity!.multipleChoice!.isCorrect(value, index); + widget.currentActivity.content.isCorrect(value, index); currentRecordModel?.addResponse( text: value, @@ -79,7 +81,7 @@ class MultipleChoiceActivityState extends State { ); // If the selected choice is correct, send the record and get the next activity - if (widget.currentActivity!.multipleChoice!.isCorrect(value, index)) { + if (widget.currentActivity.content.isCorrect(value, index)) { widget.practiceCardController.onActivityFinish(); } @@ -90,39 +92,37 @@ class MultipleChoiceActivityState extends State { @override Widget build(BuildContext context) { - final PracticeActivityModel? practiceActivity = widget.currentActivity; - - if (practiceActivity == null) { - return const SizedBox(); - } + final PracticeActivityModel practiceActivity = widget.currentActivity; return Container( padding: const EdgeInsets.all(8), child: Column( children: [ Text( - practiceActivity.multipleChoice!.question, + practiceActivity.content.question, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), + if (practiceActivity.activityType == + ActivityTypeEnum.wordFocusListening) + WordAudioButton(text: practiceActivity.content.answer), ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", originalSpan: "placeholder", onPressed: updateChoice, selectedChoiceIndex: selectedChoiceIndex, - choices: practiceActivity.multipleChoice!.choices + choices: practiceActivity.content.choices .mapIndexed( (index, value) => Choice( text: value, color: currentRecordModel?.hasTextResponse(value) ?? false - ? practiceActivity.multipleChoice!.choiceColor(index) + ? practiceActivity.content.choiceColor(index) : null, - isGold: practiceActivity.multipleChoice! - .isCorrect(value, index), + isGold: practiceActivity.content.isCorrect(value, index), ), ) .toList(), diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 7e80e2aaa..8ac0664b3 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -277,7 +277,14 @@ class MessagePracticeActivityCardState extends State { case ActivityTypeEnum.multipleChoice: return MultipleChoiceActivity( practiceCardController: this, - currentActivity: currentActivity, + currentActivity: currentActivity!, + ); + case ActivityTypeEnum.wordFocusListening: + // return WordFocusListeningActivity( + // activity: currentActivity!, practiceCardController: this); + return MultipleChoiceActivity( + practiceCardController: this, + currentActivity: currentActivity!, ); default: ErrorHandler.logError( diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index f22e097e4..e358614f3 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -58,17 +58,9 @@ class TargetTokensController { return _targetTokens = []; } - _targetTokens = []; - for (int i = 0; i < tokens.length; i++) { - //don't bother with tokens that we don't save to vocab - if (!tokens[i].lemma.saveVocab) { - continue; - } - - _targetTokens!.add(tokens[i].emptyTokenWithXP); - } - - return _targetTokens!; + return _targetTokens = tokens + .map((token) => token.emptyTokenWithXP) + .toList(); } Future updateTokensWithConstructs( @@ -84,6 +76,12 @@ class TargetTokensController { _targetTokens ??= await _initialize(context, 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){ + continue; + } + for (final construct in token.constructs) { final constructUseModel = constructList.getConstructUses( construct.id.lemma, diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart new file mode 100644 index 000000000..bdc76caaf --- /dev/null +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -0,0 +1,69 @@ +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class WordAudioButton extends StatefulWidget { + final String text; + + const WordAudioButton({ + super.key, + required this.text, + }); + + @override + WordAudioButtonState createState() => WordAudioButtonState(); +} + +class WordAudioButtonState extends State { + bool _isPlaying = false; + + TtsController ttsController = TtsController(); + + @override + @override + void initState() { + // TODO: implement initState + super.initState(); + ttsController.setupTTS().then((value) => setState(() {})); + } + + @override + Widget build(BuildContext context) { + 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 ttsController.tts.stop(); + setState(() { + _isPlaying = false; + }); + } else { + setState(() { + _isPlaying = true; + }); + await ttsController.speak(widget.text); + setState(() { + _isPlaying = false; + }); + } + }, // Disable button if language isn't supported + ), + ttsController.missingVoiceButton, + ], + ); + } +} diff --git a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart new file mode 100644 index 000000000..720f784ba --- /dev/null +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -0,0 +1,173 @@ +import 'dart:developer'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class WordFocusListeningActivity extends StatefulWidget { + final PracticeActivityModel activity; + final MessagePracticeActivityCardState practiceCardController; + + const WordFocusListeningActivity({ + super.key, + required this.activity, + required this.practiceCardController, + }); + + @override + WordFocusListeningActivityState createState() => + WordFocusListeningActivityState(); + + ActivityContent get activityContent => activity.content; +} + +class WordFocusListeningActivityState + extends State { + int? selectedChoiceIndex; + + TtsController tts = TtsController(); + + final double buttonSize = 40; + + PracticeActivityRecordModel? get currentRecordModel => + widget.practiceCardController.currentCompletionRecord; + + initializeTTS() async { + tts.setupTTS().then((value) => setState(() {})); + } + + @override + void initState() { + super.initState(); + initializeTTS(); + } + + void checkAnswer(int index) { + final String value = widget.activityContent.choices[index]; + + if (currentRecordModel?.hasTextResponse(value) ?? false) { + return; + } + + final bool isCorrect = widget.activity.content.isCorrect(value, index); + + currentRecordModel?.addResponse( + text: value, + score: isCorrect ? 1 : 0, + ); + + if (currentRecordModel == null || + currentRecordModel!.latestResponse == null) { + debugger(when: kDebugMode); + return; + } + + MatrixState.pangeaController.myAnalytics.setState( + AnalyticsStream( + // note - this maybe should be the activity event id + eventId: + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + constructs: currentRecordModel!.latestResponse!.toUses( + widget.practiceCardController.currentActivity!, + widget.practiceCardController.metadata, + ), + ), + ); + setState(() { + selectedChoiceIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + children: [ + // Text question at the top + Text( + widget.activityContent.question, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + // Blank slot for the answer + DragTarget( + builder: (context, candidateData, rejectedData) { + return CircleAvatar( + radius: buttonSize, + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppConfig.primaryColor.withOpacity(0.4), + width: 2, + style: BorderStyle.solid, + ), + ), + ), + ); + }, + onAcceptWithDetails: (details) => checkAnswer(details.data), + ), + const SizedBox(height: 10), + // Audio options as draggable buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + widget.activityContent.choices.length, + (index) => Draggable( + data: index, + feedback: _buildAudioButton(context, theme, index), + childWhenDragging: _buildAudioButton(context, theme, index, true), + child: _buildAudioButton(context, theme, index), + ), + ), + ), + ], + ); + } + + // Helper method to build the audio buttons + Widget _buildAudioButton( + BuildContext context, + ThemeData theme, + int index, [ + bool dragging = false, + ]) { + final isAnswerCorrect = widget.activityContent.isCorrect( + widget.activityContent.choices[index], + index, + ); + Color buttonColor; + if (selectedChoiceIndex == index) { + buttonColor = isAnswerCorrect + ? theme.colorScheme.secondary.withOpacity(0.7) // Correct: Green + : theme.colorScheme.error.withOpacity(0.7); // Incorrect: Red + } else { + buttonColor = + AppConfig.primaryColor.withOpacity(0.4); // Default: Primary color + } + + return GestureDetector( + onTap: () => tts.speak(widget.activityContent.choices[index]), + child: CircleAvatar( + radius: buttonSize, + backgroundColor: dragging ? Colors.grey.withOpacity(0.5) : buttonColor, + child: const Icon(Icons.play_arrow), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 47b695fb9..c97a460b3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,6 +17,7 @@ import firebase_messaging import flutter_app_badger import flutter_local_notifications import flutter_secure_storage_macos +import flutter_tts import flutter_web_auth_2 import flutter_webrtc import geolocator_apple @@ -54,6 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bb56964d1..8ca2f83e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + android_intent_plus: + dependency: "direct main" + description: + name: android_intent_plus + sha256: "38921ec22ebb3b9a7eb678792cf6fab0b6f458b61b9d327688573449c9b47db3" + url: "https://pub.dev" + source: hosted + version: "5.2.0" animations: dependency: "direct main" description: @@ -829,6 +837,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d + url: "https://pub.dev" + source: hosted + version: "4.0.2" flutter_typeahead: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 39a7e9700..77dd5ad5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,7 @@ dependencies: wakelock_plus: ^1.2.2 webrtc_interface: ^1.0.13 # #Pangea + android_intent_plus: ^5.2.0 country_picker: ^2.0.25 csv: ^6.0.0 fl_chart: ^0.67.0 @@ -128,6 +129,7 @@ dependencies: shimmer: ^3.0.0 syncfusion_flutter_xlsio: ^25.1.40 rive: 0.11.11 + flutter_tts: ^4.0.2 # Pangea# dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8fbbffa18..f8f7f9c80 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + FlutterTtsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTtsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); PasteboardPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 315ce5112..f55c3d296 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows firebase_core flutter_secure_storage_windows + flutter_tts flutter_webrtc pasteboard permission_handler_windows From 689252c17f33174d2615d8f6e8e0563aa3205fa3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 12:54:28 -0400 Subject: [PATCH 004/115] initial work on updating bot settings UI --- assets/l10n/intl_en.arb | 13 +- lib/pangea/constants/model_keys.dart | 3 + lib/pangea/models/bot_options_model.dart | 14 + ...sation_bot_custom_system_prompt_input.dart | 95 ----- .../conversation_bot_custom_zone.dart | 57 --- ...rsation_bot_discussion_keywords_input.dart | 74 ---- ...nversation_bot_discussion_topic_input.dart | 73 ---- .../conversation_bot_discussion_zone.dart | 70 ---- .../conversation_bot_dynamic_zone_label.dart | 27 -- .../conversation_bot_dynamic_zone_title.dart | 31 -- .../conversation_bot_mode_dynamic_zone.dart | 76 ++-- .../conversation_bot_mode_select.dart | 69 ++-- .../conversation_bot_settings.dart | 358 +++++++++++------- .../conversation_bot_settings_form.dart | 98 +++-- .../conversation_bot_text_adventure_zone.dart | 11 +- .../space/language_level_dropdown.dart | 81 ++-- 16 files changed, 410 insertions(+), 740 deletions(-) delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart delete mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 83de9e423..555f319de 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3904,7 +3904,6 @@ "listen": "Listen", "addConversationBot": "Enable Conversation Bot", "addConversationBotDesc": "Add a bot to this chat", - "convoBotSettingsTitle": "Conversation Bot Settings", "convoBotSettingsDescription": "Edit conversation topic and difficulty", "enterAConversationTopic": "Enter a conversation topic", "conversationTopic": "Conversation topic", @@ -4009,7 +4008,7 @@ "accuracy": "Accuracy", "points": "Points", "noPaymentInfo": "No payment info necessary!", - "conversationBotModeSelectDescription": "Bot mode", + "conversationBotModeSelectDescription": "Chat Activity", "conversationBotModeSelectOption_discussion": "Discussion", "conversationBotModeSelectOption_custom": "Custom", "conversationBotModeSelectOption_conversation": "Conversation", @@ -4030,7 +4029,7 @@ "conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt", "conversationBotCustomZone_customSystemPromptEmptyError": "Missing custom system prompt", "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction", - "botConfig": "Conversation Bot Settings", + "botConfig": "Chat Settings", "addConversationBotDialogTitleInvite": "Confirm inviting conversation bot", "addConversationBotButtonInvite": "Invite", "addConversationBotDialogInviteConfirmation": "Invite", @@ -4038,7 +4037,7 @@ "addConversationBotButtonRemove": "Remove", "addConversationBotDialogRemoveConfirmation": "Remove", "conversationBotConfigConfirmChange": "Confirm", - "conversationBotStatus": "Bot Status", + "conversationBotStatus": "Invite bot", "conversationBotTextAdventureZone_title": "Text Adventure", "conversationBotTextAdventureZone_instructionLabel": "Game Master Instructions", "conversationBotTextAdventureZone_instructionPlaceholder": "Set game master instructions", @@ -4356,5 +4355,9 @@ "grammarCopyCase": "Case", "grammarCopyDefinite": "Definiteness", "grammarCopyNumForm": "Numeral Form", - "grammarCopyUnknown": "Unknown" + "grammarCopyUnknown": "Unknown", + "enterPrompt": "Please enter a system prompt", + "selectBotLanguage": "Select bot language", + "chooseVoice": "Choose a voice", + "enterLanguageLevel": "Please enter a language level" } \ No newline at end of file diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 962532c9f..424c07830 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -121,6 +121,9 @@ class ModelKey { static const String textAdventureGameMasterInstructions = "text_adventure_game_master_instructions"; + static const String targetLanguage = "target_language"; + static const String targetVoice = "target_voice"; + static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; diff --git a/lib/pangea/models/bot_options_model.dart b/lib/pangea/models/bot_options_model.dart index db2725edc..5bae119c3 100644 --- a/lib/pangea/models/bot_options_model.dart +++ b/lib/pangea/models/bot_options_model.dart @@ -21,6 +21,8 @@ class BotOptionsModel { bool? customTriggerReactionEnabled; String? customTriggerReactionKey; String? textAdventureGameMasterInstructions; + String? targetLanguage; + String? targetVoice; BotOptionsModel({ //////////////////////////////////////////////////////////////////////////// @@ -31,6 +33,8 @@ class BotOptionsModel { this.keywords = const [], this.safetyModeration = true, this.mode = BotMode.discussion, + this.targetLanguage, + this.targetVoice, //////////////////////////////////////////////////////////////////////////// // Discussion Mode Options @@ -63,6 +67,8 @@ class BotOptionsModel { : null, safetyModeration: json[ModelKey.safetyModeration] ?? true, mode: json[ModelKey.mode] ?? BotMode.discussion, + targetLanguage: json[ModelKey.targetLanguage], + targetVoice: json[ModelKey.targetVoice], ////////////////////////////////////////////////////////////////////////// // Discussion Mode Options @@ -97,6 +103,8 @@ class BotOptionsModel { data[ModelKey.languageLevel] = languageLevel; data[ModelKey.safetyModeration] = safetyModeration; data[ModelKey.mode] = mode; + data[ModelKey.targetLanguage] = targetLanguage; + data[ModelKey.targetVoice] = targetVoice; data[ModelKey.discussionTopic] = discussionTopic; data[ModelKey.discussionKeywords] = discussionKeywords; data[ModelKey.discussionTriggerReactionEnabled] = @@ -153,6 +161,12 @@ class BotOptionsModel { case ModelKey.textAdventureGameMasterInstructions: textAdventureGameMasterInstructions = value; break; + case ModelKey.targetLanguage: + targetLanguage = value; + break; + case ModelKey.targetVoice: + targetVoice = value; + break; default: throw Exception('Invalid key for bot options - $key'); } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart deleted file mode 100644 index 2e79e0677..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotCustomSystemPromptInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotCustomSystemPromptInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String customSystemPrompt = initialBotOptions.customSystemPrompt ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: customSystemPrompt); - - final GlobalKey customSystemPromptFormKey = - GlobalKey(); - - void setBotCustomSystemPromptAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)!.conversationBotCustomZone_customSystemPromptLabel, - ), - content: Form( - key: customSystemPromptFormKey, - child: TextFormField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - if (value.isNotEmpty) { - customSystemPrompt = value; - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'This field cannot be empty'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (customSystemPromptFormKey.currentState!.validate()) { - if (customSystemPrompt != - initialBotOptions.customSystemPrompt) { - initialBotOptions.customSystemPrompt = customSystemPrompt; - onChanged.call(initialBotOptions); - } - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotCustomSystemPromptAction, - title: Text( - initialBotOptions.customSystemPrompt ?? - L10n.of(context)! - .conversationBotCustomZone_customSystemPromptPlaceholder, - ), - subtitle: customSystemPrompt.isEmpty - ? Text( - L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError, - style: const TextStyle(color: Colors.red), - ) - : null, - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart deleted file mode 100644 index 14b05dc90..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotCustomZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotCustomZone({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotCustomZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotCustomZone_customSystemPromptLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotCustomSystemPromptInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - CheckboxListTile( - title: Text( - L10n.of(context)! - .conversationBotCustomZone_customTriggerReactionEnabledLabel, - ), - enabled: false, - value: initialBotOptions.customTriggerReactionEnabled ?? true, - onChanged: (value) { - initialBotOptions.customTriggerReactionEnabled = value ?? true; - initialBotOptions.customTriggerReactionKey = - "⏩"; // hard code this for now - onChanged.call(initialBotOptions); - }, - // make this input disabled always - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart deleted file mode 100644 index fa08a860d..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionKeywordsInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionKeywordsInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String discussionKeywords = initialBotOptions.discussionKeywords ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: discussionKeywords); - - void setBotDiscussionKeywordsAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsLabel, - ), - content: TextField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - discussionKeywords = value; - }, - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (discussionKeywords == "") return; - if (discussionKeywords != - initialBotOptions.discussionKeywords) { - initialBotOptions.discussionKeywords = discussionKeywords; - onChanged.call(initialBotOptions); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotDiscussionKeywordsAction, - title: Text( - initialBotOptions.discussionKeywords ?? - L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsPlaceholder, - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart deleted file mode 100644 index c2d4eefcc..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionTopicInput extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionTopicInput({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - String discussionTopic = initialBotOptions.discussionTopic ?? ""; - - final TextEditingController textFieldController = - TextEditingController(text: discussionTopic); - - void setBotDiscussionTopicAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) => AlertDialog( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicLabel, - ), - content: TextField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - discussionTopic = value; - }, - ), - actions: [ - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () { - if (discussionTopic == "") return; - if (discussionTopic != initialBotOptions.discussionTopic) { - initialBotOptions.discussionTopic = discussionTopic; - onChanged.call(initialBotOptions); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - return ListTile( - onTap: setBotDiscussionTopicAction, - title: Text( - initialBotOptions.discussionTopic ?? - L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicPlaceholder, - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart deleted file mode 100644 index 6035faf4d..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_discussion_zone.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class ConversationBotDiscussionZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - // call this to update propagate changes to parents - final void Function(BotOptionsModel) onChanged; - - const ConversationBotDiscussionZone({ - super.key, - required this.initialBotOptions, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotDiscussionZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotDiscussionZone_discussionTopicLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotDiscussionTopicInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotDiscussionZone_discussionKeywordsLabel, - ), - Padding( - padding: const EdgeInsets.all(8), - child: ConversationBotDiscussionKeywordsInput( - initialBotOptions: initialBotOptions, - onChanged: onChanged, - ), - ), - const SizedBox(height: 12), - CheckboxListTile( - title: Text( - L10n.of(context)! - .conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel, - ), - enabled: false, - value: initialBotOptions.discussionTriggerReactionEnabled ?? true, - onChanged: (value) { - initialBotOptions.discussionTriggerReactionEnabled = value ?? true; - initialBotOptions.discussionTriggerReactionKey = - "⏩"; // hard code this for now - onChanged.call(initialBotOptions); - }, - // make this input disabled always - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart deleted file mode 100644 index 6c2043dcd..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConversationBotDynamicZoneLabel extends StatelessWidget { - final String label; - - const ConversationBotDynamicZoneLabel({ - super.key, - required this.label, - }); - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 0, 0), - child: Text( - label, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart deleted file mode 100644 index dbfbb00dc..000000000 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class ConversationBotDynamicZoneTitle extends StatelessWidget { - final String title; - - const ConversationBotDynamicZoneTitle({ - super.key, - required this.title, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: 12), - Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - const Divider( - color: Colors.grey, - thickness: 1, - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index 90d7ed789..76aec7d62 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -1,44 +1,74 @@ import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart'; import 'package:flutter/material.dart'; - -import 'conversation_bot_discussion_zone.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeDynamicZone extends StatelessWidget { final BotOptionsModel initialBotOptions; - final void Function(BotOptionsModel) onChanged; + final GlobalKey formKey; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; const ConversationBotModeDynamicZone({ super.key, required this.initialBotOptions, - required this.onChanged, + required this.formKey, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, }); @override Widget build(BuildContext context) { - final zoneMap = { - BotMode.discussion: ConversationBotDiscussionZone( - initialBotOptions: initialBotOptions, - onChanged: onChanged, + final discussionChildren = [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotDiscussionZone_discussionTopicPlaceholder, + ), + controller: discussionTopicController, ), - BotMode.custom: ConversationBotCustomZone( - initialBotOptions: initialBotOptions, - onChanged: onChanged, + const SizedBox(height: 12), + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotDiscussionZone_discussionKeywordsPlaceholder, + ), + controller: discussionKeywordsController, ), - }; - if (!zoneMap.containsKey(initialBotOptions.mode)) { - return Container(); - } - return Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, + ]; + + final customChildren = [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.of(context)! + .conversationBotCustomZone_customSystemPromptPlaceholder, ), - borderRadius: const BorderRadius.all(Radius.circular(10)), + validator: (value) => value == null || value.isEmpty + ? L10n.of(context)!.enterPrompt + : null, + controller: customSystemPromptController, ), - child: zoneMap[initialBotOptions.mode], + ]; + + return Column( + children: [ + if (initialBotOptions.mode == BotMode.discussion) ...discussionChildren, + if (initialBotOptions.mode == BotMode.custom) ...customChildren, + const SizedBox(height: 12), + CheckboxListTile( + title: Text( + L10n.of(context)! + .conversationBotCustomZone_customTriggerReactionEnabledLabel, + ), + enabled: false, + value: initialBotOptions.customTriggerReactionEnabled ?? true, + onChanged: null, + ), + const SizedBox(height: 12), + ], ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index 753a8a8a8..c22801d25 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -24,56 +24,35 @@ class ConversationBotModeSelect extends StatelessWidget { // L10n.of(context)!.conversationBotModeSelectOption_storyGame, }; - return Padding( - padding: const EdgeInsets.all(12.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: DropdownButton( - // Initial Value - hint: Padding( - padding: const EdgeInsets.only(left: 15), + String? mode = initialMode; + if (!options.containsKey(initialMode)) { + mode = null; + } + + return DropdownButtonFormField( + // Initial Value + hint: Text( + options[mode ?? BotMode.discussion]!, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + // ), + isExpanded: true, + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + // Array list of items + items: [ + for (final entry in options.entries) + DropdownMenuItem( + value: entry.key, child: Text( - options[initialMode ?? BotMode.discussion]!, - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), + entry.value, overflow: TextOverflow.clip, textAlign: TextAlign.center, ), ), - isExpanded: true, - underline: Container(), - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items - items: [ - for (final entry in options.entries) - DropdownMenuItem( - value: entry.key, - child: Padding( - padding: const EdgeInsets.only(left: 15), - child: Text( - entry.value, - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - ), - ), - ], - onChanged: onChanged, - ), - ), + ], + onChanged: onChanged, ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index fe85b48c7..3a8cc53ff 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -39,6 +39,13 @@ class ConversationBotSettingsState extends State { ConversationBotSettingsState({Key? key}); + final TextEditingController discussionTopicController = + TextEditingController(); + final TextEditingController discussionKeywordsController = + TextEditingController(); + final TextEditingController customSystemPromptController = + TextEditingController(); + @override void initState() { super.initState(); @@ -55,6 +62,10 @@ class ConversationBotSettingsState extends State { ? Matrix.of(context).client.getRoomById(widget.activeSpaceId!) : null; isCreating = widget.room == null; + + discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; + discussionTopicController.text = botOptions.discussionTopic ?? ""; + customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; } Future setBotOption() async { @@ -88,6 +99,106 @@ class ConversationBotSettingsState extends State { ); } + Future showBotOptionsDialog() async { + if (isCreating) return; + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) => Dialog( + child: Form( + key: formKey, + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints( + maxWidth: 450, + maxHeight: 725, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: ConversationBotSettingsDialog( + addBot: addBot, + botOptions: botOptions, + formKey: formKey, + updateAddBot: (bool value) => + setState(() => addBot = value), + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, + ), + ), + ), + ), + ), + ); + }, + ); + + if (confirm == true) { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + + updateBotOption(() => botOptions = botOptions); + + final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; + if (addBot && !isBotRoomMember) { + await widget.room?.invite(BotName.byEnvironment); + } else if (!addBot && isBotRoomMember) { + await widget.room?.kick(BotName.byEnvironment); + } + } + } + + Future showNewRoomBotOptionsDialog() async { + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: addBot + ? Text( + L10n.of(context)!.addConversationBotButtonTitleRemove, + ) + : Text( + L10n.of(context)!.addConversationBotDialogTitleInvite, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(L10n.of(context)!.cancel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(!addBot); + }, + child: addBot + ? Text( + L10n.of(context)! + .addConversationBotDialogRemoveConfirmation, + ) + : Text( + L10n.of(context)! + .addConversationBotDialogInviteConfirmation, + ), + ), + ], + ); + }, + ); + + if (confirm == true) { + setState(() => addBot = true); + widget.room?.invite(BotName.byEnvironment); + } else { + setState(() => addBot = false); + widget.room?.kick(BotName.byEnvironment); + } + } + + final GlobalKey formKey = GlobalKey(); + @override Widget build(BuildContext context) { return AnimatedContainer( @@ -119,162 +230,115 @@ class ConversationBotSettingsState extends State { ), trailing: isCreating ? ElevatedButton( - onPressed: () async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: addBot - ? Text( - L10n.of(context)! - .addConversationBotButtonTitleRemove, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogTitleInvite, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(!addBot); - }, - child: addBot - ? Text( - L10n.of(context)! - .addConversationBotDialogRemoveConfirmation, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogInviteConfirmation, - ), - ), - ], - ); - }, - ); - - if (confirm == true) { - setState(() => addBot = true); - widget.room?.invite(BotName.byEnvironment); - } else { - setState(() => addBot = false); - widget.room?.kick(BotName.byEnvironment); - } - }, - child: addBot - ? Text( - L10n.of(context)!.addConversationBotButtonRemove, - ) - : Text( - L10n.of(context)!.addConversationBotButtonInvite, - ), + onPressed: showNewRoomBotOptionsDialog, + child: Text( + addBot + ? L10n.of(context)!.addConversationBotButtonRemove + : L10n.of(context)!.addConversationBotButtonInvite, + ), ) : const Icon(Icons.settings), - onTap: isCreating - ? null - : () async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text( - L10n.of(context)!.botConfig, - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: - const EdgeInsets.fromLTRB(0, 0, 0, 12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - L10n.of(context)!.conversationBotStatus, - ), - Switch( - value: addBot, - onChanged: (value) { - setState( - () => addBot = value, - ); - }, - ), - ], - ), - ), - if (addBot) - Flexible( - child: SingleChildScrollView( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .colorScheme - .secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - ), - child: ConversationBotSettingsForm( - botOptions: botOptions, - ), - ), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text( - L10n.of(context)! - .conversationBotConfigConfirmChange, - ), - ), - ], - ), - ); - }, - ); - if (confirm == true) { - updateBotOption(() { - botOptions = botOptions; - }); - final bool isBotRoomMember = - await widget.room?.botIsInRoom ?? false; - if (addBot && !isBotRoomMember) { - await widget.room?.invite(BotName.byEnvironment); - } else if (!addBot && isBotRoomMember) { - await widget.room?.kick(BotName.byEnvironment); - } - } - }, + onTap: showBotOptionsDialog, ), if (isCreating && addBot) ConversationBotSettingsForm( botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, ), ], ), ); } } + +class ConversationBotSettingsDialog extends StatelessWidget { + final bool addBot; + final BotOptionsModel botOptions; + final GlobalKey formKey; + + final void Function(bool) updateAddBot; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; + + const ConversationBotSettingsDialog({ + super.key, + required this.addBot, + required this.botOptions, + required this.formKey, + required this.updateAddBot, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SwitchListTile( + title: Text( + L10n.of(context)!.conversationBotStatus, + ), + value: addBot, + onChanged: updateAddBot, + contentPadding: const EdgeInsets.all(4), + ), + if (addBot) + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 20), + ConversationBotSettingsForm( + botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: customSystemPromptController, + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(L10n.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: () { + final isValid = formKey.currentState!.validate(); + if (!isValid) return; + Navigator.of(context).pop(true); + }, + child: Text(L10n.of(context)!.confirm), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index b630f608b..fb482ab1c 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -3,15 +3,25 @@ import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart'; import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotSettingsForm extends StatefulWidget { final BotOptionsModel botOptions; + final GlobalKey formKey; + + final TextEditingController discussionTopicController; + final TextEditingController discussionKeywordsController; + final TextEditingController customSystemPromptController; const ConversationBotSettingsForm({ super.key, required this.botOptions, + required this.formKey, + required this.discussionTopicController, + required this.discussionKeywordsController, + required this.customSystemPromptController, }); @override @@ -21,8 +31,6 @@ class ConversationBotSettingsForm extends StatefulWidget { class ConversationBotSettingsFormState extends State { - final formKey = GlobalKey(); - late BotOptionsModel botOptions; @override @@ -35,17 +43,48 @@ class ConversationBotSettingsFormState Widget build(BuildContext context) { return Column( children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - L10n.of(context)!.conversationLanguageLevel, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - fontSize: 16, - ), + DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.selectBotLanguage, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, ), + value: botOptions.targetLanguage, + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down), + items: MatrixState.pangeaController.pLanguageStore.targetOptions + .map((language) { + return DropdownMenuItem( + value: language.langCode, + child: Text( + language.getDisplayName(context) ?? language.langCode, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ); + }).toList(), + onChanged: (String? newValue) => { + setState(() => botOptions.targetLanguage = newValue!), + }, ), + const SizedBox(height: 12), + DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.chooseVoice, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + value: botOptions.targetVoice, + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down), + items: const [], + onChanged: (String? newValue) => { + setState(() => botOptions.targetVoice = newValue!), + }, + ), + const SizedBox(height: 12), LanguageLevelDropdown( initialLevel: botOptions.languageLevel, onChanged: (int? newValue) => { @@ -53,15 +92,21 @@ class ConversationBotSettingsFormState botOptions.languageLevel = newValue!; }), }, + validator: (value) => + value == null ? L10n.of(context)!.enterLanguageLevel : null, ), - Text( - L10n.of(context)!.conversationBotModeSelectDescription, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - fontSize: 16, + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + L10n.of(context)!.conversationBotModeSelectDescription, + style: Theme.of(context).textTheme.titleLarge, + ), ), ), + const SizedBox(height: 12), ConversationBotModeSelect( initialMode: botOptions.mode, onChanged: (String? mode) => { @@ -70,18 +115,13 @@ class ConversationBotSettingsFormState }), }, ), - Padding( - padding: const EdgeInsets.all(12), - child: ConversationBotModeDynamicZone( - initialBotOptions: botOptions, - onChanged: (BotOptionsModel? newOptions) { - if (newOptions != null) { - setState(() { - botOptions = newOptions; - }); - } - }, - ), + const SizedBox(height: 12), + ConversationBotModeDynamicZone( + initialBotOptions: botOptions, + discussionTopicController: widget.discussionTopicController, + discussionKeywordsController: widget.discussionKeywordsController, + customSystemPromptController: widget.customSystemPromptController, + formKey: widget.formKey, ), ], ); diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart index effbf70ee..ed2c41486 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart @@ -1,10 +1,8 @@ import 'package:fluffychat/pangea/models/bot_options_model.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; +// TODO check how this looks class ConversationBotTextAdventureZone extends StatelessWidget { final BotOptionsModel initialBotOptions; // call this to update propagate changes to parents @@ -20,13 +18,6 @@ class ConversationBotTextAdventureZone extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - ConversationBotDynamicZoneTitle( - title: L10n.of(context)!.conversationBotTextAdventureZone_title, - ), - ConversationBotDynamicZoneLabel( - label: L10n.of(context)! - .conversationBotTextAdventureZone_instructionLabel, - ), Padding( padding: const EdgeInsets.all(8), child: ConversationBotGameMasterInstructionsInput( diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index aeb1cfd36..0964a49ab 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -6,71 +6,44 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class LanguageLevelDropdown extends StatelessWidget { final int? initialLevel; final void Function(int?)? onChanged; + final String? Function(int?)? validator; const LanguageLevelDropdown({ super.key, this.initialLevel, this.onChanged, + this.validator, }); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - width: 0.5, - ), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: DropdownButton( - // Initial Value - hint: Padding( - padding: const EdgeInsets.only(left: 15), - child: Text( - initialLevel == null - ? L10n.of(context)!.selectLanguageLevel - : LanguageLevelTextPicker.languageLevelText( - context, - initialLevel!, - ), - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, + return DropdownButtonFormField( + // Initial Value + hint: Text( + L10n.of(context)!.selectLanguageLevel, + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + value: initialLevel, + isExpanded: true, + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + // Array list of items + items: LanguageLevelType.allInts.map((int levelOption) { + return DropdownMenuItem( + value: levelOption, + child: Text( + LanguageLevelTextPicker.languageLevelText( + context, + levelOption, ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, ), - isExpanded: true, - underline: Container(), - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items - items: LanguageLevelType.allInts.map((int levelOption) { - return DropdownMenuItem( - value: levelOption, - child: Text( - LanguageLevelTextPicker.languageLevelText( - context, - levelOption, - ), - style: const TextStyle().copyWith( - color: Theme.of(context).textTheme.bodyLarge!.color, - fontSize: 14, - ), - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - ); - }).toList(), - // After selecting the desired option,it will - // change button value to selected value - onChanged: onChanged, - ), - ), + ); + }).toList(), + onChanged: onChanged, + validator: validator, ); } } From 4d2f36890faa6f2b829429b5f457bc6b72b7d834 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 14:40:54 -0400 Subject: [PATCH 005/115] additional error handling in retrieval of practice events --- .../pangea_message_event.dart | 22 +++++++++++++------ .../practice_activity/word_audio_button.dart | 18 +++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 13da5ace8..3b3a4c1db 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -587,18 +587,27 @@ class PangeaMessageEvent { /// Returns a list of all [PracticeActivityEvent] objects /// associated with this message event. List get _practiceActivityEvents { - return _latestEdit + final List events = _latestEdit .aggregatedEvents( timeline, PangeaEventTypes.pangeaActivity, ) - .map( - (e) => PracticeActivityEvent( + .toList(); + + final List practiceEvents = []; + for (final event in events) { + try { + practiceEvents.add( + PracticeActivityEvent( timeline: timeline, - event: e, + event: event, ), - ) - .toList(); + ); + } catch (e, s) { + ErrorHandler.logError(e: e, s: s, data: event.toJson()); + } + } + return practiceEvents; } /// Returns a boolean value indicating whether there are any @@ -617,7 +626,6 @@ class PangeaMessageEvent { String langCode, { bool debug = false, }) { - // @wcjord - disabled try catch for testing try { debugger(when: debug); final List activities = []; diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index bdc76caaf..8602a48d8 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -48,17 +48,17 @@ class WordAudioButtonState extends State { onPressed: () async { if (_isPlaying) { await ttsController.tts.stop(); - setState(() { - _isPlaying = false; - }); + if (mounted) { + setState(() => _isPlaying = false); + } } else { - setState(() { - _isPlaying = true; - }); + if (mounted) { + setState(() => _isPlaying = true); + } await ttsController.speak(widget.text); - setState(() { - _isPlaying = false; - }); + if (mounted) { + setState(() => _isPlaying = false); + } } }, // Disable button if language isn't supported ), From 3a6d6de7c592eeab0593705659b1e608b0226391 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 14:46:03 -0400 Subject: [PATCH 006/115] UI fixes for new groups, form validation for discussion topic --- assets/l10n/intl_en.arb | 3 +- lib/pages/new_group/new_group.dart | 14 ++++++ .../conversation_bot_mode_dynamic_zone.dart | 3 ++ .../conversation_bot_settings.dart | 45 ++++++++++++++----- .../conversation_bot_settings_form.dart | 3 +- 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 555f319de..648153894 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4359,5 +4359,6 @@ "enterPrompt": "Please enter a system prompt", "selectBotLanguage": "Select bot language", "chooseVoice": "Choose a voice", - "enterLanguageLevel": "Please enter a language level" + "enterLanguageLevel": "Please enter a language level", + "enterDiscussionTopic": "Please enter a discussion topic" } \ No newline at end of file diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 3d8301ec5..5d6a17012 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -106,6 +106,20 @@ class NewGroupController extends State { // #Pangea // validate init bot options + if (addConversationBotKey.currentState?.formKey.currentState != null) { + final isValid = addConversationBotKey + .currentState!.formKey.currentState! + .validate(); + if (isValid == false) { + setState(() { + error = L10n.of(context)! + .conversationBotCustomZone_customSystemPromptEmptyError; + loading = false; + }); + return; + } + } + addConversationBotKey.currentState?.updateAllBotOptions(); final addBot = addConversationBotKey.currentState?.addBot ?? false; if (addBot) { final botOptions = addConversationBotKey.currentState!.botOptions; diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index 76aec7d62..dc1997a20 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -29,6 +29,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionTopicPlaceholder, ), controller: discussionTopicController, + validator: (value) => value == null || value.isEmpty + ? L10n.of(context)!.enterDiscussionTopic + : null, ), const SizedBox(height: 12), TextFormField( diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 3a8cc53ff..963949a78 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -99,6 +99,12 @@ class ConversationBotSettingsState extends State { ); } + void updateAllBotOptions() { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + } + Future showBotOptionsDialog() async { if (isCreating) return; final bool? confirm = await showDialog( @@ -135,10 +141,7 @@ class ConversationBotSettingsState extends State { ); if (confirm == true) { - botOptions.discussionTopic = discussionTopicController.text; - botOptions.discussionKeywords = discussionKeywordsController.text; - botOptions.customSystemPrompt = customSystemPromptController.text; - + updateAllBotOptions(); updateBotOption(() => botOptions = botOptions); final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; @@ -241,12 +244,34 @@ class ConversationBotSettingsState extends State { onTap: showBotOptionsDialog, ), if (isCreating && addBot) - ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + Form( + key: formKey, + child: ConversationBotSettingsForm( + botOptions: botOptions, + formKey: formKey, + discussionKeywordsController: + discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: + customSystemPromptController, + ), + ), + ], + ), ), ], ), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index fb482ab1c..5447b67bb 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -95,7 +95,7 @@ class ConversationBotSettingsFormState validator: (value) => value == null ? L10n.of(context)!.enterLanguageLevel : null, ), - const SizedBox(height: 20), + const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Padding( @@ -106,7 +106,6 @@ class ConversationBotSettingsFormState ), ), ), - const SizedBox(height: 12), ConversationBotModeSelect( initialMode: botOptions.mode, onChanged: (String? mode) => { From 427460073294c034cf61f23a99b45436e4c01aae Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 16:02:42 -0400 Subject: [PATCH 007/115] initial work to bring back search bar --- lib/pages/chat_list/chat_list_header.dart | 222 ++++++++-------- .../chat_list/client_chooser_button.dart | 247 +++++++----------- 2 files changed, 214 insertions(+), 255 deletions(-) diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 4c018355c..1f7a1fbe9 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -23,7 +23,10 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { return SliverAppBar( floating: true, - toolbarHeight: 175, + // #Pangea + // toolbarHeight: 72, + toolbarHeight: controller.isSearchMode ? 72 : 175, + // Pangea# pinned: FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, @@ -32,111 +35,124 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { // selectMode == SelectMode.normal ? Colors.transparent : null, // Pangea# automaticallyImplyLeading: false, - leading: selectMode == SelectMode.normal - ? null - : IconButton( - tooltip: L10n.of(context)!.cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelAction, - color: theme.colorScheme.primary, - ), - title: selectMode == SelectMode.share - ? Text( - L10n.of(context)!.share, - key: const ValueKey(SelectMode.share), - ) + // #Pangea + // leading: selectMode == SelectMode.normal + // ? null + // : IconButton( + // tooltip: L10n.of(context)!.cancel, + // icon: const Icon(Icons.close_outlined), + // onPressed: controller.cancelAction, + // color: theme.colorScheme.primary, + // ), + // Pangea# + title: // #Pangea - : Column( - children: [ - ClientChooserButton(controller), - const LearningProgressIndicators(), - ], + // selectMode == SelectMode.share + // ? Text( + // L10n.of(context)!.share, + // key: const ValueKey(SelectMode.share), + // ) + // : + // Pangea# + Column( + children: [ + TextField( + controller: controller.searchController, + focusNode: controller.searchFocusNode, + textInputAction: TextInputAction.search, + onChanged: (text) => controller.onSearchEnter( + text, + globalSearch: globalSearch, ), - // : 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, - // ), - // ), - // suffixIcon: controller.isSearchMode && globalSearch - // ? controller.isSearching - // ? const Padding( - // padding: EdgeInsets.symmetric( - // vertical: 10.0, - // horizontal: 12, - // ), - // child: SizedBox.square( - // dimension: 24, - // child: CircularProgressIndicator.adaptive( - // strokeWidth: 2, - // ), - // ), - // ) - // : TextButton.icon( - // onPressed: controller.setServer, - // style: TextButton.styleFrom( - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(99), - // ), - // textStyle: const TextStyle(fontSize: 12), - // ), - // icon: const Icon(Icons.edit_outlined, size: 16), - // label: Text( - // controller.searchServer ?? - // Matrix.of(context).client.homeserver!.host, - // maxLines: 2, - // ), - // ) - // : SizedBox( - // width: 0, - // child: ClientChooserButton(controller), - // ), - // ), - // ), + 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, + ), + ), + suffixIcon: controller.isSearchMode && globalSearch + ? controller.isSearching + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + // #Pangea + : SizedBox( + width: 0, + child: ClientChooserButton(controller), + ) + // : TextButton.icon( + // onPressed: controller.setServer, + // style: TextButton.styleFrom( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(99), + // ), + // textStyle: const TextStyle(fontSize: 12), + // ), + // icon: const Icon(Icons.edit_outlined, size: 16), + // label: Text( + // controller.searchServer ?? + // Matrix.of(context).client.homeserver!.host, + // maxLines: 2, + // ), + // ) + // Pangea# + : SizedBox( + width: 0, + child: ClientChooserButton(controller), + ), + ), + ), + if (!controller.isSearchMode) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: LearningProgressIndicators(), + ), + ], + ), + // #Pangea + // actions: selectMode == SelectMode.share + // ? [ + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // vertical: 8.0, + // ), + // child: ClientChooserButton(controller), + // ), + // ] + // : null, // Pangea# - actions: selectMode == SelectMode.share - ? [ - // #Pangea - // Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 16.0, - // vertical: 8.0, - // ), - // child: ClientChooserButton(controller), - // ), - // Pangea# - ] - : null, ); } diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 0a406fa2c..578542806 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,15 +1,15 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import '../../utils/fluffy_share.dart'; import 'chat_list.dart'; class ClientChooserButton extends StatelessWidget { @@ -41,45 +41,30 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - // PopupMenuItem( - // enabled: matrix.client.rooms.any( - // (room) => - // room.isSpace && - // room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin, - // ), - // value: SettingsAction.spaceAnalytics, - // child: Row( - // children: [ - // const Icon(Icons.analytics_outlined), - // const SizedBox(width: 18), - // Expanded(child: Text(L10n.of(context)!.spaceAnalytics)), - // ], - // ), - // ), - // PopupMenuItem( - // enabled: matrix.client.rooms.any( - // (room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom, - // ), - // value: SettingsAction.myAnalytics, - // child: Row( - // children: [ - // const Icon(Icons.analytics_outlined), - // const SizedBox(width: 18), - // Expanded(child: Text(L10n.of(context)!.myLearning)), - // ], - // ), - // ), - // PopupMenuItem( - // value: SettingsAction.newGroup, - // child: Row( - // children: [ - // const Icon(Icons.group_add_outlined), - // const SizedBox(width: 18), - // Text(L10n.of(context)!.createGroup), - // ], - // ), - // ), + PopupMenuItem( + value: SettingsAction.learning, + child: Row( + children: [ + const Icon(Icons.psychology_outlined), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.learningSettings)), + ], + ), + ), // Pangea# + PopupMenuItem( + value: SettingsAction.newGroup, + child: Row( + children: [ + const Icon(Icons.group_add_outlined), + const SizedBox(width: 18), + // #Pangea + Expanded(child: Text(L10n.of(context)!.createGroup)), + // Text(L10n.of(context)!.createGroup), + // Pangea# + ], + ), + ), PopupMenuItem( value: SettingsAction.newSpace, child: Row( @@ -87,7 +72,7 @@ class ClientChooserButton extends StatelessWidget { const Icon(Icons.workspaces_outlined), const SizedBox(width: 18), // #Pangea - Expanded(child: Text(L10n.of(context)!.createNewSpace)), + Text(L10n.of(context)!.createNewSpace), // Text(L10n.of(context)!.createNewSpace), // Pangea# ], @@ -123,36 +108,34 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.archive_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.archive), + Text(L10n.of(context)!!.archive), ], ), ),*/ - // #Pangea PopupMenuItem( - value: SettingsAction.learning, + value: SettingsAction.settings, child: Row( children: [ - const Icon(Icons.psychology_outlined), + const Icon(Icons.settings_outlined), const SizedBox(width: 18), - Expanded(child: Text(L10n.of(context)!.learningSettings)), + // #Pangea + Text(L10n.of(context)!.settings), + // Text(L10n.of(context)!.settings), + // Pangea# ], ), ), - // Pangea# + // #Pangea PopupMenuItem( - value: SettingsAction.settings, + value: SettingsAction.logout, child: Row( children: [ - const Icon(Icons.settings_outlined), + const Icon(Icons.logout_outlined), const SizedBox(width: 18), - // #Pangea - // Text(L10n.of(context)!.settings), - Expanded(child: Text(L10n.of(context)!.settings)), - // Pangea# + Expanded(child: Text(L10n.of(context)!.logout)), ], ), ), - // #Pangea // const PopupMenuDivider(), // for (final bundle in bundles) ...[ // if (matrix.accountBundles[bundle]!.length != 1 || @@ -223,16 +206,6 @@ class ClientChooserButton extends StatelessWidget { // ], // ), // ), - PopupMenuItem( - value: SettingsAction.logout, - child: Row( - children: [ - const Icon(Icons.logout_outlined), - const SizedBox(width: 18), - Expanded(child: Text(L10n.of(context)!.logout)), - ], - ), - ), // Pangea# ]; } @@ -248,65 +221,49 @@ class ClientChooserButton extends StatelessWidget { builder: (context, snapshot) => Stack( alignment: Alignment.center, children: [ - // #Pangea - // ...List.generate( - // clientCount, - // (index) => KeyBoardShortcuts( - // keysToPress: _buildKeyboardShortcut(index + 1), - // helpLabel: L10n.of(context)!.switchToAccount(index + 1), - // onKeysPressed: () => _handleKeyboardShortcut( - // matrix, - // index, - // context, - // ), - // child: const SizedBox.shrink(), - // ), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.nextAccount, - // onKeysPressed: () => _nextAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.shiftLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.previousAccount, - // onKeysPressed: () => _previousAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - ClipRRect( - borderRadius: BorderRadius.circular(16), + ...List.generate( + clientCount, + (index) => KeyBoardShortcuts( + keysToPress: _buildKeyboardShortcut(index + 1), + helpLabel: L10n.of(context)!.switchToAccount(index + 1), + onKeysPressed: () => _handleKeyboardShortcut( + matrix, + index, + context, + ), + child: const SizedBox.shrink(), + ), + ), + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.tab, + }, + helpLabel: L10n.of(context)!.nextAccount, + onKeysPressed: () => _nextAccount(matrix, context), + child: const SizedBox.shrink(), + ), + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.tab, + }, + helpLabel: L10n.of(context)!.previousAccount, + onKeysPressed: () => _previousAccount(matrix, context), + child: const SizedBox.shrink(), + ), + PopupMenuButton( + onSelected: (o) => _clientSelected(o, context), + itemBuilder: _bundleMenuItems, child: Material( color: Colors.transparent, - child: - // Pangea# - PopupMenuButton( - onSelected: (o) => _clientSelected(o, context), - itemBuilder: _bundleMenuItems, - // #Pangea - child: ListTile( - mouseCursor: SystemMouseCursors.click, - leading: const Icon(Icons.settings_outlined), - title: Text(L10n.of(context)!.mainMenu), - ), - // 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, - // ), - // ), - // Pangea# + borderRadius: BorderRadius.circular(99), + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + matrix.client.userID!.localpart, + size: 32, ), ), ), @@ -353,42 +310,30 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.newSpace: controller.createNewSpace(); break; - case SettingsAction.invite: - FluffyShare.shareInviteLink(context); - break; + // #Pangea + // case SettingsAction.invite: + // FluffyShare.shareInviteLink(context); + // break; + // Pangea# case SettingsAction.settings: context.go('/rooms/settings'); break; - case SettingsAction.archive: - context.go('/rooms/archive'); - break; - case SettingsAction.setStatus: - controller.setStatus(); // #Pangea + // case SettingsAction.archive: + // context.go('/rooms/archive'); + // break; + // case SettingsAction.setStatus: + // controller.setStatus(); + // break; case SettingsAction.learning: context.go('/rooms/settings/learning'); break; - case SettingsAction.newClass: - context.go('/rooms/newspace'); - break; case SettingsAction.joinWithClassCode: SpaceCodeUtil.joinWithSpaceCodeDialog( context, MatrixState.pangeaController, ); break; - case SettingsAction.findAConversationPartner: - findConversationPartnerDialog( - context, - MatrixState.pangeaController, - ); - break; - // case SettingsAction.spaceAnalytics: - // context.go('/rooms/analytics'); - // break; - // case SettingsAction.myAnalytics: - // context.go('/rooms/mylearning'); - // break; case SettingsAction.logout: pLogoutAction(context); break; @@ -471,17 +416,15 @@ enum SettingsAction { addAccount, newGroup, newSpace, - setStatus, - invite, + // #Pangea + // setStatus, + // invite, + // Pangea# settings, - archive, // #Pangea - learning, + // archive, joinWithClassCode, - // spaceAnalytics, - // myAnalytics, - findAConversationPartner, + learning, logout, - newClass, // Pangea# } From 8a24b0342bbb6c49c840b35d2207bb5845e7d403 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 16:33:30 -0400 Subject: [PATCH 008/115] added fix for different map keys in voices across platforms --- lib/pangea/extensions/pangea_event_extension.dart | 14 ++++++++++---- lib/pangea/widgets/chat/message_audio_card.dart | 6 ------ lib/pangea/widgets/chat/tts_controller.dart | 13 +++++-------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart index 23a0c1374..1ca280a98 100644 --- a/lib/pangea/extensions/pangea_event_extension.dart +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -45,18 +45,24 @@ extension PangeaEvent on Event { Future getPangeaAudioFile() async { if (type != EventTypes.Message || messageType != MessageTypes.Audio) { ErrorHandler.logError( - e: "Event $eventId is not an audio message", + e: "Event is not an audio message", + data: { + "event": toJson(), + }, ); return null; } - // @ggurdin what are cases where these would be null? - // if it would be unexpected, we should log an error with details to investigate final transcription = content.tryGetMap(ModelKey.transcription); final audioContent = content.tryGetMap('org.matrix.msc1767.audio'); - if (transcription == null || audioContent == null) return null; + if (transcription == null || audioContent == null) { + ErrorHandler.logError( + e: "Called getPangeaAudioFile on an audio message without transcription or audio content", + ); + return null; + } final matrixFile = await downloadAndDecryptAttachment(); final duration = audioContent.tryGet('duration'); diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index b56e7103e..7bb557d00 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -56,12 +56,6 @@ class MessageAudioCardState extends State { @override void didUpdateWidget(covariant oldWidget) { - // @ggurdin did you find a case of needing to reinitialize TTS because of a language change? - // if (widget.messageEvent.messageDisplayLangCode != - // oldWidget.messageEvent.messageDisplayLangCode) { - // initializeTTS(); - // } - if (oldWidget.selection != widget.selection) { debugPrint('selection changed'); setSectionStartAndEndFromSelection(); diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index e8edd65c3..ea28a66dd 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -35,15 +35,13 @@ class TtsController { await tts.awaitSpeakCompletion(true); final voices = await tts.getVoices; + debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { - // debugPrint('v: $v'); - - //@ggurdin i changed this from name to locale - //in my testing, that's where the language code is stored - // maybe it's different for different devices? was it different in your android testing? - // return v['name']?.split("-").first; - return v['locale']?.split("-").first; + // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' + final nameCode = v['name']?.split("-").first; + final localeCode = v['locale']?.split("-").first; + return nameCode.length == 2 ? nameCode : localeCode; }) .toSet() .cast() @@ -67,7 +65,6 @@ class TtsController { bool get isLanguageFullySupported => availableLangCodes.contains(targetLanguage); - // @ggurdin Widget get missingVoiceButton => targetLanguage != null && (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) ? const SizedBox.shrink() From bc20769daa3a3d34dfd467271d174e7d71cdd056 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 16:39:34 -0400 Subject: [PATCH 009/115] dont give old clients new activity types and catch old activity content better --- lib/main.dart | 2 +- ...actice_activity_generation_controller.dart | 93 +++++++------------ lib/pangea/enum/activity_type_enum.dart | 14 +-- .../pangea_message_event.dart | 21 +---- .../practice_activity_event.dart | 1 - .../message_activity_request.dart | 5 + lib/pangea/widgets/chat/tts_controller.dart | 17 +++- 7 files changed, 61 insertions(+), 92 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6be6edc91..36add47dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.local_choreo"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index a8d7cca36..410f8eeaa 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -5,13 +5,10 @@ import 'dart:developer'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/network/requests.dart'; import 'package:fluffychat/pangea/network/urls.dart'; @@ -22,11 +19,11 @@ import 'package:matrix/matrix.dart'; /// Represents an item in the completion cache. class _RequestCacheItem { MessageActivityRequest req; - PracticeActivityModel? practiceActivityEvent; + PracticeActivityModel? practiceActivity; _RequestCacheItem({ required this.req, - required this.practiceActivityEvent, + required this.practiceActivity, }); } @@ -109,64 +106,46 @@ class PracticeGenerationController { final int cacheKey = req.hashCode; if (_cache.containsKey(cacheKey)) { - return _cache[cacheKey]!.practiceActivityEvent; - } else { - //TODO - send request to server/bot, either via API or via event of type pangeaActivityReq - // for now, just make and send the event from the client - final MessageActivityResponse res = await _fetch( - accessToken: _pangeaController.userController.accessToken, - requestModel: req, - ); + return _cache[cacheKey]!.practiceActivity; + } - if (res.finished) { - debugPrint('Activity generation finished'); - return null; - } + final MessageActivityResponse res = await _fetch( + accessToken: _pangeaController.userController.accessToken, + requestModel: req, + ); - // if the server points to an existing event, return that event - if (res.existingActivityEventId != null) { - final Event? existingEvent = - await event.room.getEventById(res.existingActivityEventId!); - - debugPrint( - 'Existing activity event found: ${existingEvent?.content}', - ); - if (existingEvent != null) { - return PracticeActivityEvent( - event: existingEvent, - timeline: event.timeline, - ).practiceActivity; - } - } + if (res.finished) { + debugPrint('Activity generation finished'); + return null; + } + + // if the server points to an existing event, return that event + if (res.existingActivityEventId != null) { + final Event? existingEvent = + await event.room.getEventById(res.existingActivityEventId!); - if (res.activity == null) { - debugPrint('No activity generated'); - return null; + debugPrint( + 'Existing activity event found: ${existingEvent?.content}', + ); + if (existingEvent != null) { + return PracticeActivityEvent( + event: existingEvent, + timeline: event.timeline, + ).practiceActivity; } + } + + if (res.activity == null) { + debugPrint('No activity generated'); + return null; + } - debugPrint('Activity generated: ${res.activity!.toJson()}'); + debugPrint('Activity generated: ${res.activity!.toJson()}'); - _sendAndPackageEvent(res.activity!, event); - _cache[cacheKey] = - _RequestCacheItem(req: req, practiceActivityEvent: res.activity!); + _sendAndPackageEvent(res.activity!, event); + _cache[cacheKey] = + _RequestCacheItem(req: req, practiceActivity: res.activity!); - return _cache[cacheKey]!.practiceActivityEvent; - } + return _cache[cacheKey]!.practiceActivity; } - - PracticeActivityModel _dummyModel(PangeaMessageEvent event) => - PracticeActivityModel( - tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab), - ], - activityType: ActivityTypeEnum.multipleChoice, - langCode: event.messageDisplayLangCode, - msgId: event.eventId, - content: ActivityContent( - question: "What is a synonym for 'happy'?", - choices: ["sad", "angry", "joyful", "tired"], - answer: "joyful", - spanDisplayDetails: null, - ), - ); } diff --git a/lib/pangea/enum/activity_type_enum.dart b/lib/pangea/enum/activity_type_enum.dart index eace349d2..66bfb3e61 100644 --- a/lib/pangea/enum/activity_type_enum.dart +++ b/lib/pangea/enum/activity_type_enum.dart @@ -1,22 +1,10 @@ -enum ActivityTypeEnum { - multipleChoice, - freeResponse, - listening, - speaking, - wordFocusListening -} +enum ActivityTypeEnum { multipleChoice, wordFocusListening } extension ActivityTypeExtension on ActivityTypeEnum { String get string { switch (this) { case ActivityTypeEnum.multipleChoice: return 'multiple_choice'; - case ActivityTypeEnum.freeResponse: - return 'free_response'; - case ActivityTypeEnum.listening: - return 'listening'; - case ActivityTypeEnum.speaking: - return 'speaking'; case ActivityTypeEnum.wordFocusListening: return 'word_focus_listening'; } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 3b3a4c1db..324c4a018 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -603,6 +603,7 @@ class PangeaMessageEvent { event: event, ), ); + final content = practiceEvents.last.practiceActivity; } catch (e, s) { ErrorHandler.logError(e: e, s: s, data: event.toJson()); } @@ -625,22 +626,10 @@ class PangeaMessageEvent { List practiceActivitiesByLangCode( String langCode, { bool debug = false, - }) { - try { - debugger(when: debug); - final List activities = []; - for (final event in _practiceActivityEvents) { - if (event.practiceActivity.langCode == langCode) { - activities.add(event); - } - } - return activities; - } catch (e, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, s: s, data: event.toJson()); - return []; - } - } + }) => + _practiceActivityEvents + .where((event) => event.practiceActivity.langCode == langCode) + .toList(); /// Returns a list of [PracticeActivityEvent] for the user's active l2. List get practiceActivities => diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index f8ac678dd..5ab1cce31 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -41,7 +41,6 @@ class PracticeActivityEvent { return _content!; } catch (e, s) { final contentMap = event.content; - debugger(when: kDebugMode); rethrow; } } 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 6261a0215..0740fb8c3 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -242,6 +242,11 @@ class MessageActivityRequest { 'existing_activities': existingActivities.map((e) => e.toJson()).toList(), 'activity_quality_feedback': activityQualityFeedback?.toJson(), 'iso_8601_time_of_req': DateTime.now().toIso8601String(), + // this is a list of activity types that the client can handle + // 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(), }; } diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index ea28a66dd..2ced099c6 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -14,7 +14,6 @@ class TtsController { List availableLangCodes = []; final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts(); - // if targetLanguage isn't set here, it needs to be set later TtsController() { setupTTS(); } @@ -26,8 +25,6 @@ class TtsController { debugger(when: kDebugMode && targetLanguage == null); - debugPrint('setupTTS targetLanguage: $targetLanguage'); - tts.setLanguage( targetLanguage ?? "en", ); @@ -38,16 +35,28 @@ class TtsController { debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { +<<<<<<< Updated upstream // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; final localeCode = v['locale']?.split("-").first; return nameCode.length == 2 ? nameCode : localeCode; +======= + debugPrint('v: $v'); + + //@ggurdin i changed this from name to locale + //in my testing, that's where the language code is stored + // maybe it's different for different devices? was it different in your android testing? + // return v['name']?.split("-").first; + return v['locale']?.split("-").first; +>>>>>>> Stashed changes }) .toSet() .cast() .toList(); - debugPrint("lang supported? $isLanguageFullySupported"); + debugPrint("availableLangCodes: $availableLangCodes"); + + debugger(when: kDebugMode && !isLanguageFullySupported); } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s); From db61a1ae6928c290d13a789bc00b81ce355a8291 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 16:41:12 -0400 Subject: [PATCH 010/115] merging in changes to tts and fixing env filename again --- lib/main.dart | 2 +- lib/pangea/widgets/chat/tts_controller.dart | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 36add47dc..6be6edc91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env.local_choreo"); + await dotenv.load(fileName: ".env"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 2ced099c6..9acc7f7f8 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -35,20 +35,10 @@ class TtsController { debugPrint("voices: $voices"); availableLangCodes = (voices as List) .map((v) { -<<<<<<< Updated upstream // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; final localeCode = v['locale']?.split("-").first; return nameCode.length == 2 ? nameCode : localeCode; -======= - debugPrint('v: $v'); - - //@ggurdin i changed this from name to locale - //in my testing, that's where the language code is stored - // maybe it's different for different devices? was it different in your android testing? - // return v['name']?.split("-").first; - return v['locale']?.split("-").first; ->>>>>>> Stashed changes }) .toSet() .cast() From 89a61c03eda337a7da119b347df203a015565749 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 21 Oct 2024 17:06:09 -0400 Subject: [PATCH 011/115] go back to fluffychat new space/group pages, delete add to space toggles widget --- lib/config/routes.dart | 6 +- lib/pages/chat_details/chat_details.dart | 5 +- lib/pages/chat_details/chat_details_view.dart | 8 - lib/pages/new_group/new_group.dart | 157 +--------- lib/pages/new_group/new_group_view.dart | 108 +++---- lib/pages/new_space/new_space.dart | 278 ++++++------------ lib/pages/new_space/new_space_view.dart | 83 ++---- .../widgets/class/add_space_toggles.dart | 268 ----------------- lib/utils/file_selector.dart | 78 +++++ pubspec.lock | 36 ++- pubspec.yaml | 1 + 11 files changed, 271 insertions(+), 757 deletions(-) delete mode 100644 lib/pangea/widgets/class/add_space_toggles.dart create mode 100644 lib/utils/file_selector.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 0ee135e7d..515b25fd9 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -233,11 +233,7 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - NewGroup( - // #Pangea - spaceId: state.uri.queryParameters['spaceId'], - // Pangea# - ), + const NewGroup(), ), redirect: loggedOutRedirect, // #Pangea diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 050a1b272..a94430da1 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; import 'package:fluffychat/pangea/utils/set_class_name.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -41,9 +40,7 @@ class ChatDetailsController extends State { String? get roomId => widget.roomId; // #Pangea - final GlobalKey addToSpaceKey = GlobalKey(); - final GlobalKey - addConversationBotKey = + final GlobalKey addConversationBotKey = GlobalKey(); bool displayAddStudentOptions = false; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index edf277e86..90f2a8927 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_inv import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/lock_room.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -395,13 +394,6 @@ class ChatDetailsView extends StatelessWidget { room: room, ), const Divider(height: 1), - if (!room.isDirectChat && room.isRoomAdmin) - AddToSpaceToggles( - roomId: room.id, - key: controller.addToSpaceKey, - startOpen: false, - ), - const Divider(height: 1), ListTile( title: Text( L10n.of(context)!.leave, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 5d6a17012..df5f14aa0 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -1,33 +1,14 @@ import 'dart:typed_data'; -import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/new_group/new_group_view.dart'; -import 'package:fluffychat/pangea/constants/bot_mode.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/models/chat_topic_model.dart'; -import 'package:fluffychat/pangea/models/lemma.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; -import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; +import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; class NewGroup extends StatefulWidget { - // #Pangea - final String? spaceId; - - const NewGroup({ - super.key, - this.spaceId, - }); - // Pangea# + const NewGroup({super.key}); @override NewGroupController createState() => NewGroupController(); @@ -47,50 +28,25 @@ class NewGroupController extends State { bool loading = false; - // #Pangea - PangeaController pangeaController = MatrixState.pangeaController; - final GlobalKey addToSpaceKey = GlobalKey(); - final GlobalKey addConversationBotKey = - GlobalKey(); - final GlobalKey addCapacityKey = - GlobalKey(); - - ChatTopic chatTopic = ChatTopic.empty; - - void setVocab(List vocab) => setState(() => chatTopic.vocab = vocab); - - String? get activeSpaceId => - GoRouterState.of(context).uri.queryParameters['spaceId']; - // Pangea# - void setPublicGroup(bool b) => setState(() => publicGroup = b); void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b); void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, + final photo = await selectFiles( + context, + type: FileSelectorType.images, allowMultiple: false, - withData: true, ); + final bytes = await photo.singleOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } void submitAction([_]) async { - // #Pangea - if (nameController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.emptyChatNameWarning), - ), - ); - return; - } - // Pangea# final client = Matrix.of(context).client; try { @@ -104,67 +60,12 @@ class NewGroupController extends State { if (!mounted) return; - // #Pangea - // validate init bot options - if (addConversationBotKey.currentState?.formKey.currentState != null) { - final isValid = addConversationBotKey - .currentState!.formKey.currentState! - .validate(); - if (isValid == false) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError; - loading = false; - }); - return; - } - } - addConversationBotKey.currentState?.updateAllBotOptions(); - final addBot = addConversationBotKey.currentState?.addBot ?? false; - if (addBot) { - final botOptions = addConversationBotKey.currentState!.botOptions; - if (botOptions.mode == BotMode.custom) { - if (botOptions.customSystemPrompt == null || - botOptions.customSystemPrompt!.isEmpty) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_customSystemPromptEmptyError; - loading = false; - }); - return; - } - } else if (botOptions.mode == BotMode.textAdventure) { - if (botOptions.textAdventureGameMasterInstructions == null || - botOptions.textAdventureGameMasterInstructions!.isEmpty) { - setState(() { - error = L10n.of(context)! - .conversationBotCustomZone_instructionSystemPromptEmptyError; - loading = false; - }); - return; - } - } - } - // Pangea# - final roomId = await client.createGroupChat( - // #Pangea - // visibility: - // publicGroup ? sdk.Visibility.public : sdk.Visibility.private, - // preset: publicGroup - // ? sdk.CreateRoomPreset.publicChat - // : sdk.CreateRoomPreset.privateChat, - preset: sdk.CreateRoomPreset.publicChat, - powerLevelContentOverride: - await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ), - invite: [ - if (addConversationBotKey.currentState?.addBot ?? false) - BotName.byEnvironment, - ], - // Pangea# + visibility: + groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, groupName: nameController.text.isNotEmpty ? nameController.text : null, initialState: [ if (avatar != null) @@ -172,29 +73,9 @@ class NewGroupController extends State { type: sdk.EventTypes.RoomAvatar, content: {'url': avatarUrl.toString()}, ), - // #Pangea - if (addConversationBotKey.currentState?.addBot ?? false) - addConversationBotKey.currentState!.botOptions.toStateEvent, - // Pangea# ], ); if (!mounted) return; - if (publicGroup && groupCanBeFound) { - await client.setRoomVisibilityOnDirectory( - roomId, - visibility: sdk.Visibility.public, - ); - } - // #Pangea - GoogleAnalytics.createChat(roomId); - await addToSpaceKey.currentState!.addSpaces(roomId); - - final capacity = addCapacityKey.currentState?.capacity; - final room = client.getRoomById(roomId); - if (capacity != null && room != null) { - room.updateRoomCapacity(capacity); - } - // Pangea# context.go('/rooms/$roomId/invite'); } catch (e, s) { sdk.Logs().d('Unable to create group', e, s); @@ -205,20 +86,6 @@ class NewGroupController extends State { } } - //#Pangea - @override - void initState() { - Future.delayed(Duration.zero, () { - chatTopic.langCode = - pangeaController.languageController.userL2?.langCode ?? - pangeaController.pLanguageStore.targetOptions.first.langCode; - setState(() {}); - }); - - super.initState(); - } - //Pangea# - @override Widget build(BuildContext context) => NewGroupView(this); } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 6dfcec212..addf7b5f7 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -1,8 +1,5 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; -import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -27,20 +24,8 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - // #Pangea - // title: Text(L10n.of(context)!.createGroup), - title: Text(L10n.of(context)!.createChat), - // Pangea# + title: Text(L10n.of(context)!.createGroup), ), - // #Pangea - floatingActionButton: FloatingActionButton.extended( - onPressed: controller.loading ? null : controller.submitAction, - icon: controller.loading ? null : const Icon(Icons.chat_bubble_outline), - label: controller.loading - ? const CircularProgressIndicator.adaptive() - : Text(L10n.of(context)!.createChat), - ), - // Pangea# body: MaxWidthBody( child: Column( mainAxisSize: MainAxisSize.min, @@ -68,9 +53,6 @@ class NewGroupView extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( - // #Pangea - maxLength: 64, - // Pangea# autofocus: true, controller: controller.nameController, autocorrect: false, @@ -85,40 +67,31 @@ class NewGroupView extends StatelessWidget { ), ), const SizedBox(height: 16), - // #Pangea - RoomCapacityButton( - key: controller.addCapacityKey, + SwitchListTile.adaptive( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + secondary: const Icon(Icons.public_outlined), + title: Text(L10n.of(context)!.groupIsPublic), + value: controller.publicGroup, + onChanged: controller.loading ? null : controller.setPublicGroup, ), - ConversationBotSettings( - key: controller.addConversationBotKey, - activeSpaceId: controller.activeSpaceId, - ), - const Divider(height: 1), - AddToSpaceToggles( - key: controller.addToSpaceKey, - startOpen: true, - activeSpaceId: controller.activeSpaceId, + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: controller.publicGroup + ? SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(horizontal: 32), + secondary: const Icon(Icons.search_outlined), + title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), + value: controller.groupCanBeFound, + onChanged: controller.loading + ? null + : controller.setGroupCanBeFound, + ) + : const SizedBox.shrink(), ), + // #Pangea // SwitchListTile.adaptive( - // secondary: const Icon(Icons.public_outlined), - // title: Text(L10n.of(context)!.groupIsPublic), - // value: controller.publicGroup, - // onChanged: controller.loading ? null : controller.setPublicGroup, - // ), - // AnimatedSize( - // duration: FluffyThemes.animationDuration, - // child: controller.publicGroup - // ? SwitchListTile.adaptive( - // secondary: const Icon(Icons.search_outlined), - // title: Text(L10n.of(context)!.groupCanBeFoundViaSearch), - // value: controller.groupCanBeFound, - // onChanged: controller.loading - // ? null - // : controller.setGroupCanBeFound, - // ) - // : const SizedBox.shrink(), - // ), - // SwitchListTile.adaptive( + // contentPadding: const EdgeInsets.symmetric(horizontal: 32), // secondary: Icon( // Icons.lock_outlined, // color: theme.colorScheme.onSurface, @@ -132,29 +105,20 @@ class NewGroupView extends StatelessWidget { // value: !controller.publicGroup, // onChanged: null, // ), - // Padding( - // padding: const EdgeInsets.all(16.0), - // child: SizedBox( - // width: double.infinity, - // child: ElevatedButton( - // onPressed: - // controller.loading ? null : controller.submitAction, - // child: controller.loading - // ? const LinearProgressIndicator() - // : Row( - // children: [ - // Expanded( - // child: Text( - // L10n.of(context)!.createGroupAndInviteUsers, - // ), - // ), - // Icon(Icons.adaptive.arrow_forward_outlined), - // ], - // ), - // ), - // ), - // ), // Pangea# + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + controller.loading ? null : controller.submitAction, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.createGroupAndInviteUsers), + ), + ), + ), AnimatedSize( duration: FluffyThemes.animationDuration, child: error == null diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 0a7b809d8..e89159900 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,20 +1,19 @@ -import 'package:file_picker/file_picker.dart'; +import 'dart:typed_data'; + import 'package:fluffychat/pages/new_space/new_space_view.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; -import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/utils/file_selector.dart'; +import 'package:fluffychat/utils/localized_exception_extension.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'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; @@ -28,48 +27,32 @@ class NewSpace extends StatefulWidget { class NewSpaceController extends State { TextEditingController nameController = TextEditingController(); TextEditingController topicController = TextEditingController(); - // #Pangea bool publicGroup = false; - // bool publicGroup = true; - // final GlobalKey rulesEditorKey = GlobalKey(); - final GlobalKey addToSpaceKey = GlobalKey(); - // commenting out language settings in spaces for now - // final GlobalKey languageSettingsKey = - // GlobalKey(); - final GlobalKey addCapacityKey = - GlobalKey(); - - //Pangea# bool loading = false; - // #Pangea - // String? nameError; - // String? topicError; - // Pangea# + String? nameError; + String? topicError; Uint8List? avatar; Uri? avatarUrl; void selectPhoto() async { - final photo = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: false, - withData: true, + final photo = await selectFiles( + context, + type: FileSelectorType.images, ); - + final bytes = await photo.firstOrNull?.readAsBytes(); setState(() { avatarUrl = null; - avatar = photo?.files.singleOrNull?.bytes; + avatar = bytes; }); } void setPublicGroup(bool b) => setState(() => publicGroup = b); // #Pangea - List get initialState { - final events = []; - - events.add( + List initialState(String joinCode) { + return [ StateEvent( type: EventTypes.RoomPowerLevels, stateKey: '', @@ -84,191 +67,94 @@ class NewSpaceController extends State { }, }, ), - ); - - // commenting out pangea room rules in spaces for now - // if (rulesEditorKey.currentState?.rules != null) { - // events.add(rulesEditorKey.currentState!.rules.toStateEvent); - // } else { - // debugger(when: kDebugMode); - // } - // commenting out language settings in spaces for now - // if (languageSettingsKey.currentState != null) { - // events - // .add(languageSettingsKey.currentState!.languageSettings.toStateEvent); - // } - - return events; + StateEvent( + type: sdk.EventTypes.RoomJoinRules, + content: { + ModelKey.joinRule: + sdk.JoinRules.knock.toString().replaceAll('JoinRules.', ''), + ModelKey.accessCode: joinCode, + }, + ), + ]; } //Pangea# void submitAction([_]) async { final client = Matrix.of(context).client; setState(() { - // #Pangea - // nameError = topicError = null; - // Pangea# + nameError = topicError = null; }); - // #Pangea - // commenting out pangea room rules in spaces for now - // if (rulesEditorKey.currentState == null) { - // debugger(when: kDebugMode); - // return; - // } - // commenting out language settings in spaces for now - // if (languageSettingsKey.currentState != null && - // languageSettingsKey.currentState!.sameLanguages) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: Text(L10n.of(context)!.noIdenticalLanguages), - // ), - // ); - // return; - // } - // final int? languageLevel = - // languageSettingsKey.currentState!.languageSettings.languageLevel; - // if (languageLevel == null) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)), - // ); - // return; - // } - // Pangea# if (nameController.text.isEmpty) { setState(() { - // #Pangea - // nameError = L10n.of(context)!.pleaseChoose; - final String warning = L10n.of(context)!.emptySpaceNameWarning; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(warning)), - ); - // Pangea# + nameError = L10n.of(context)!.pleaseChoose; }); return; } setState(() { loading = true; }); - // #Pangea - // try { - await showFutureLoadingDialog( - context: context, - future: () async { - try { - // Pangea# - final avatar = this.avatar; - avatarUrl ??= - avatar == null ? null : await client.uploadContent(avatar); - final classCode = await SpaceCodeUtil.generateSpaceCode(client); - final spaceId = await client.createRoom( - // #Pangea - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - // #Pangea - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - // #Pangea - // roomAliasName: publicGroup - // ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') - // : null, - // roomAliasName: SpaceCodeUtil.generateSpaceCode(), - // Pangea# - name: nameController.text.trim(), - topic: topicController.text.isEmpty ? null : topicController.text, - // #Pangea - // powerLevelContentOverride: {'events_default': 100}, - powerLevelContentOverride: addToSpaceKey.currentState != null - ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( - context, - addToSpaceKey.currentState!.parent, - ) - : null, - // Pangea# - initialState: [ - // #Pangea - ...initialState, - if (avatar != null) - sdk.StateEvent( - type: sdk.EventTypes.RoomAvatar, - content: {'url': avatarUrl.toString()}, - ), - sdk.StateEvent( - type: sdk.EventTypes.RoomJoinRules, - content: { - ModelKey.joinRule: sdk.JoinRules.knock - .toString() - .replaceAll('JoinRules.', ''), - ModelKey.accessCode: classCode, - }, - ), - // Pangea# - ], - // Pangea# - ); - // #Pangea - final List> futures = [ - Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), - ]; - if (addToSpaceKey.currentState != null) { - futures.add(addToSpaceKey.currentState!.addSpaces(spaceId)); - } - await Future.wait(futures); - - final capacity = addCapacityKey.currentState?.capacity; - final space = client.getRoomById(spaceId); - if (capacity != null && space != null) { - space.updateRoomCapacity(capacity); - } - - final Room? room = Matrix.of(context).client.getRoomById(spaceId); - if (room == null) { - ErrorHandler.logError( - e: 'Failed to get new space by id $spaceId', - ); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); - return; - } + try { + final avatar = this.avatar; + avatarUrl ??= avatar == null ? null : await client.uploadContent(avatar); + // #Pangea + final joinCode = await SpaceCodeUtil.generateSpaceCode(client); + // Pangea# - GoogleAnalytics.createClass(room.name, room.classCode); - try { - await room.invite(BotName.byEnvironment); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${room.id}", - ); - } - // Pangea# - if (!mounted) return; + final spaceId = await client.createRoom( + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + roomAliasName: publicGroup + ? nameController.text.trim().toLowerCase().replaceAll(' ', '_') + : null, + name: nameController.text.trim(), + topic: topicController.text.isEmpty ? null : topicController.text, + powerLevelContentOverride: {'events_default': 100}, + initialState: [ // #Pangea - // context.pop(spaceId); - MatrixState.pangeaController.classController - .setActiveSpaceIdInChatListController(spaceId); + ...initialState(joinCode), // Pangea# - } catch (e, s) { - // #Pangea - ErrorHandler.logError(e: e, s: s); - rethrow; - // setState(() { - // topicError = e.toLocalizedString(context); - // }); - // Pangea# - } finally { - setState(() { - loading = false; - }); - } - }, - ); + if (avatar != null) + sdk.StateEvent( + type: sdk.EventTypes.RoomAvatar, + content: {'url': avatarUrl.toString()}, + ), + ], + ); + if (!mounted) return; + // #Pangea + Room? room = client.getRoomById(spaceId); + if (room == null) { + await Matrix.of(context).client.waitForRoomInSync(spaceId); + room = client.getRoomById(spaceId); + } + if (room == null) return; + GoogleAnalytics.createClass(room.name, room.classCode); + try { + await room.invite(BotName.byEnvironment); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${room.id}", + ); + } + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(spaceId); + // Pangea# + context.pop(spaceId); + } catch (e) { + setState(() { + topicError = e.toLocalizedString(context); + }); + } finally { + setState(() { + loading = false; + }); + } // TODO: Go to spaces } @override - // #Pangea - // Widget build(BuildContext context) => NewSpaceView(this); - Widget build(BuildContext context) { - return NewSpaceView(this); - } - // Pangea# + Widget build(BuildContext context) => NewSpaceView(this); } diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 087218a48..3842231c8 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,5 +1,3 @@ -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; -import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:flutter/material.dart'; @@ -19,15 +17,6 @@ class NewSpaceView extends StatelessWidget { appBar: AppBar( title: Text(L10n.of(context)!.createNewSpace), ), - // #Pangea - floatingActionButton: FloatingActionButton.extended( - onPressed: controller.loading ? null : controller.submitAction, - icon: controller.loading ? null : const Icon(Icons.workspaces_outlined), - label: controller.loading - ? const CircularProgressIndicator.adaptive() - : Text(L10n.of(context)!.createSpace), - ), - // Pangea# body: MaxWidthBody( child: Column( mainAxisSize: MainAxisSize.min, @@ -62,58 +51,38 @@ class NewSpaceView extends StatelessWidget { decoration: InputDecoration( prefixIcon: const Icon(Icons.people_outlined), labelText: L10n.of(context)!.spaceName, - // #Pangea - // errorText: controller.nameError, - // Pangea# + errorText: controller.nameError, ), ), ), const SizedBox(height: 16), - // #Pangea - RoomCapacityButton( - key: controller.addCapacityKey, - spaceMode: true, + SwitchListTile.adaptive( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + title: Text(L10n.of(context)!.spaceIsPublic), + value: controller.publicGroup, + onChanged: controller.setPublicGroup, + ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + trailing: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Icon(Icons.info_outlined), + ), + subtitle: Text(L10n.of(context)!.newSpaceDescription), ), - AddToSpaceToggles( - key: controller.addToSpaceKey, - startOpen: true, - spaceMode: true, + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + controller.loading ? null : controller.submitAction, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.createNewSpace), + ), + ), ), - // SwitchListTile.adaptive( - // title: Text(L10n.of(context)!.spaceIsPublic), - // value: controller.publicGroup, - // onChanged: controller.setPublicGroup, - // ), - // ListTile( - // trailing: const Padding( - // padding: EdgeInsets.symmetric(horizontal: 16.0), - // child: Icon(Icons.info_outlined), - // ), - // subtitle: Text(L10n.of(context)!.newSpaceDescription), - // ), - // Padding( - // padding: const EdgeInsets.all(16.0), - // child: SizedBox( - // width: double.infinity, - // child: ElevatedButton( - // onPressed: - // controller.loading ? null : controller.submitAction, - // child: controller.loading - // ? const LinearProgressIndicator() - // : Row( - // children: [ - // Expanded( - // child: Text( - // L10n.of(context)!.createNewSpace, - // ), - // ), - // Icon(Icons.adaptive.arrow_forward_outlined), - // ], - // ), - // ), - // ), - // ), - // Pangea# ], ), ), diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart deleted file mode 100644 index fd7843955..000000000 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:matrix/matrix.dart'; - -import '../../../widgets/matrix.dart'; -import '../../utils/firebase_analytics.dart'; - -//PTODO - auto invite students when you add a space and delete the add_class_and_invite.dart file -class AddToSpaceToggles extends StatefulWidget { - final String? roomId; - final bool startOpen; - final String? activeSpaceId; - final bool spaceMode; - - const AddToSpaceToggles({ - super.key, - this.roomId, - this.startOpen = false, - this.activeSpaceId, - this.spaceMode = false, - }); - - @override - AddToSpaceState createState() => AddToSpaceState(); -} - -class AddToSpaceState extends State { - late Room? room; - late Room? parent; - late List possibleParents; - late bool isOpen; - late bool isSuggested; - - AddToSpaceState({Key? key}); - - @override - void initState() { - initialize(); - super.initState(); - } - - @override - void didUpdateWidget(AddToSpaceToggles oldWidget) { - if (oldWidget.roomId != widget.roomId) { - initialize(); - } - super.didUpdateWidget(oldWidget); - } - - void initialize() { - //if roomId is null, it means this widget is being used in the creation flow - room = widget.roomId != null - ? Matrix.of(context).client.getRoomById(widget.roomId!) - : null; - - isSuggested = true; - room?.isSuggested().then((value) => isSuggested = value); - - possibleParents = Matrix.of(context) - .client - .rooms - .where( - (Room r) => r.isSpace && widget.roomId != r.id, - ) - .toList(); - - parent = widget.roomId != null - ? possibleParents.firstWhereOrNull( - (r) => r.spaceChildren.any((room) => room.roomId == widget.roomId), - ) - : null; - - //sort possibleParents - //if possibleParent in parents, put first - //use sort but use any instead of contains because contains uses == and we want to compare by id - possibleParents.sort((a, b) { - if (parent?.id == a.id) { - return -1; - } else if (parent?.id == b.id) { - return 1; - } else { - return a.name.compareTo(b.name); - } - }); - - isOpen = widget.startOpen; - - if (widget.activeSpaceId != null) { - final activeSpace = - Matrix.of(context).client.getRoomById(widget.activeSpaceId!); - if (activeSpace == null) { - ErrorHandler.logError( - e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), - ); - return; - } - if (activeSpace.canSendEvent(EventTypes.SpaceChild)) { - parent = activeSpace; - } - } - } - - Future _addSingleSpace(String roomToAddId, Room newParent) async { - GoogleAnalytics.addParent(roomToAddId, newParent.classCode); - await newParent.pangeaSetSpaceChild( - roomToAddId, - suggested: isSuggested, - ); - } - - Future addSpaces(String roomToAddId) async { - if (parent == null) return; - await _addSingleSpace(roomToAddId, parent!); - } - - Future handleAdd(bool add, Room possibleParent) async { - //in this case, the room has already been made so we handle adding as it happens - if (room != null) { - await showFutureLoadingDialog( - context: context, - future: () => add - ? _addSingleSpace(room!.id, possibleParent) - : possibleParent.removeSpaceChild(room!.id), - onError: (e) { - // if error occurs, do not change value of toggle - add = !add; - return (e as Object?)?.toLocalizedString(context) ?? - e?.toString() ?? - L10n.of(context)!.oopsSomethingWentWrong; - }, - ); - } - - setState( - () => add ? parent = possibleParent : parent = null, - ); - } - - Widget getAddToSpaceToggleItem(int index) { - final Room possibleParent = possibleParents[index]; - final bool canAdd = possibleParent.canAddAsParentOf( - room, - spaceMode: widget.spaceMode, - ); - - return Opacity( - opacity: canAdd ? 1 : 0.5, - child: Column( - children: [ - SwitchListTile.adaptive( - title: possibleParent.nameAndRoomTypeIcon(), - activeColor: AppConfig.activeToggleColor, - value: parent?.id == possibleParent.id, - onChanged: (bool add) => canAdd - ? handleAdd(add, possibleParent) - : ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.noPermission), - ), - ), - ), - Divider( - height: 0.5, - color: Theme.of(context).colorScheme.secondary.withAlpha(25), - ), - ], - ), - ); - } - - Future setSuggested(bool suggested) async { - setState(() => isSuggested = suggested); - if (room != null) { - await showFutureLoadingDialog( - context: context, - future: () async => await room?.setSuggested(suggested), - ); - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ListTile( - title: Text( - L10n.of(context)!.addToSpace, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.addSpaceToSpaceDesc - : L10n.of(context)!.addChatToSpaceDesc, - ), - leading: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, - child: const Icon(Icons.workspaces_outlined), - ), - trailing: Icon( - isOpen - ? Icons.keyboard_arrow_down_outlined - : Icons.keyboard_arrow_right_outlined, - ), - onTap: () { - setState(() => isOpen = !isOpen); - }, - ), - if (isOpen) ...[ - const Divider(height: 1), - possibleParents.isNotEmpty - ? Column( - children: [ - SwitchListTile.adaptive( - title: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.suggestToSpace - : L10n.of(context)!.suggestToChat, - ), - secondary: Icon( - isSuggested - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - subtitle: Text( - widget.spaceMode || (room?.isSpace ?? false) - ? L10n.of(context)!.suggestToSpaceDesc - : L10n.of(context)!.suggestToChatDesc, - ), - activeColor: AppConfig.activeToggleColor, - value: isSuggested, - onChanged: (bool add) => setSuggested(add), - ), - Divider( - height: 0.5, - color: - Theme.of(context).colorScheme.secondary.withAlpha(25), - ), - ...possibleParents.mapIndexed( - (index, _) => getAddToSpaceToggleItem(index), - ), - ], - ) - : Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - L10n.of(context)!.inNoSpaces, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ], - ], - ); - } -} diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart new file mode 100644 index 000000000..e9d36652c --- /dev/null +++ b/lib/utils/file_selector.dart @@ -0,0 +1,78 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:flutter/widgets.dart'; + +Future> selectFiles( + BuildContext context, { + String? title, + FileSelectorType type = FileSelectorType.any, + bool allowMultiple = false, +}) async { + if (!PlatformInfos.isLinux) { + final result = await AppLock.of(context).pauseWhile( + FilePicker.platform.pickFiles( + compressionQuality: 0, + allowMultiple: allowMultiple, + type: type.filePickerType, + allowedExtensions: type.extensions, + ), + ); + return result?.xFiles ?? []; + } + + if (allowMultiple) { + return await AppLock.of(context).pauseWhile( + openFiles( + confirmButtonText: title, + acceptedTypeGroups: type.groups, + ), + ); + } + final file = await AppLock.of(context).pauseWhile( + openFile( + confirmButtonText: title, + acceptedTypeGroups: type.groups, + ), + ); + if (file == null) return []; + return [file]; +} + +enum FileSelectorType { + any([], FileType.any, null), + images( + [ + XTypeGroup( + label: 'JPG', + extensions: ['jpg', 'JPG', 'jpeg', 'JPEG'], + ), + XTypeGroup( + label: 'PNGs', + extensions: ['png', 'PNG'], + ), + XTypeGroup( + label: 'WEBP', + extensions: ['WebP', 'WEBP'], + ), + ], + FileType.image, + null, + ), + zip( + [ + XTypeGroup( + label: 'ZIP', + extensions: ['zip', 'ZIP'], + ), + ], + FileType.custom, + ['zip', 'ZIP'], + ); + + const FileSelectorType(this.groups, this.filePickerType, this.extensions); + final List groups; + final FileType filePickerType; + final List? extensions; +} diff --git a/pubspec.lock b/pubspec.lock index 8ca2f83e7..c8a53d2c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -424,6 +424,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.6" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "00aafa9ae05a8663d0b4f17abd2a02316911ca0f46f9b9dacb9578b324d99590" + url: "https://pub.dev" + source: hosted + version: "0.5.1+9" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" + url: "https://pub.dev" + source: hosted + version: "0.5.3+1" file_selector_linux: dependency: transitive description: @@ -448,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" file_selector_windows: dependency: transitive description: @@ -2756,5 +2788,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 77dd5ad5c..69f0f1a63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 file_picker: ^8.0.6 + file_selector: ^1.0.3 flutter: sdk: flutter flutter_app_badger: ^1.5.0 From 10d937ed1bc563fe802fc3dd9a97f86b7c417255 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 21 Oct 2024 17:17:24 -0400 Subject: [PATCH 012/115] additional error handling to fix freezing and/or catch error message --- lib/pangea/widgets/chat/tts_controller.dart | 59 +++++++++++++++++-- .../practice_activity/word_audio_button.dart | 7 ++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 9acc7f7f8..93d270133 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -18,8 +18,21 @@ class TtsController { setupTTS(); } + Future dispose() async { + await tts.stop(); + } + + onError(dynamic message) => ErrorHandler.logError( + m: 'TTS error', + data: { + 'message': message, + }, + ); + Future setupTTS() async { try { + tts.setErrorHandler(onError); + targetLanguage ??= MatrixState.pangeaController.languageController.userL2?.langCode; @@ -53,12 +66,50 @@ class TtsController { } } + Future stop() async { + try { + // return type is dynamic but apparent its supposed to be 1 + // https://pub.dev/packages/flutter_tts + final result = await tts.stop(); + if (result != 1) { + ErrorHandler.logError( + m: 'Unexpected result from tts.stop', + data: { + 'result': result, + }, + ); + } + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } + await tts.stop(); + } + Future speak(String text) async { - targetLanguage ??= - MatrixState.pangeaController.languageController.userL2?.langCode; + try { + stop(); - await tts.stop(); - return tts.speak(text); + targetLanguage ??= + MatrixState.pangeaController.languageController.userL2?.langCode; + + final result = await tts.speak(text); + + // return type is dynamic but apparent its supposed to be 1 + // https://pub.dev/packages/flutter_tts + if (result != 1) { + ErrorHandler.logError( + m: 'Unexpected result from tts.speak', + data: { + 'result': result, + 'text': text, + }, + ); + } + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } } bool get isLanguageFullySupported => diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 8602a48d8..226328804 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -19,7 +19,6 @@ class WordAudioButtonState extends State { TtsController ttsController = TtsController(); - @override @override void initState() { // TODO: implement initState @@ -27,6 +26,12 @@ class WordAudioButtonState extends State { ttsController.setupTTS().then((value) => setState(() {})); } + @override + void dispose() { + ttsController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( From cb7fd0f6f607a082a963dc0a62b9244517e959e3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:10:44 -0400 Subject: [PATCH 013/115] comment out references to buggy package keyboard_shortcuts --- .../chat_list/client_chooser_button.dart | 68 ++++++++++--------- pubspec.lock | 9 --- pubspec.yaml | 16 +++-- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 578542806..b22512bbe 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +// import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; import 'chat_list.dart'; @@ -221,38 +221,40 @@ class ClientChooserButton extends StatelessWidget { builder: (context, snapshot) => Stack( alignment: Alignment.center, children: [ - ...List.generate( - clientCount, - (index) => KeyBoardShortcuts( - keysToPress: _buildKeyboardShortcut(index + 1), - helpLabel: L10n.of(context)!.switchToAccount(index + 1), - onKeysPressed: () => _handleKeyboardShortcut( - matrix, - index, - context, - ), - child: const SizedBox.shrink(), - ), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.nextAccount, - onKeysPressed: () => _nextAccount(matrix, context), - child: const SizedBox.shrink(), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.previousAccount, - onKeysPressed: () => _previousAccount(matrix, context), - child: const SizedBox.shrink(), - ), + // #Pangea + // ...List.generate( + // clientCount, + // (index) => KeyBoardShortcuts( + // keysToPress: _buildKeyboardShortcut(index + 1), + // helpLabel: L10n.of(context)!.switchToAccount(index + 1), + // onKeysPressed: () => _handleKeyboardShortcut( + // matrix, + // index, + // context, + // ), + // child: const SizedBox.shrink(), + // ), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.nextAccount, + // onKeysPressed: () => _nextAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.shiftLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.previousAccount, + // onKeysPressed: () => _previousAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // Pangea# PopupMenuButton( onSelected: (o) => _clientSelected(o, context), itemBuilder: _bundleMenuItems, diff --git a/pubspec.lock b/pubspec.lock index c8a53d2c7..0a0e1ed23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1324,15 +1324,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - keyboard_shortcuts: - dependency: "direct main" - description: - path: "." - ref: null-safety - resolved-ref: a3d4020911860ff091d90638ab708604b71d2c5a - url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" - source: git - version: "0.1.4" language_tool: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 69f0f1a63..1af58cbb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,9 @@ dependencies: image_picker: ^1.1.0 intl: any just_audio: ^0.9.39 - keyboard_shortcuts: ^0.1.4 + # #Pangea + # keyboard_shortcuts: ^0.1.4 + # Pangea# latlong2: ^0.9.1 linkify: ^5.0.0 # #Pangea @@ -216,8 +218,10 @@ dependency_overrides: version: ^1.0.1 # waiting for null safety # Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13 - keyboard_shortcuts: - git: - url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git - ref: null-safety - win32: 5.5.3 + # #Pangea + # keyboard_shortcuts: + # git: + # url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git + # ref: null-safety + # win32: 5.5.3 + # Pangea# From 0d75e961dabdd435cff57dd7f3684c66455b231d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:13:38 -0400 Subject: [PATCH 014/115] changed list from const to final to prevent unsupported operation error on .add() --- lib/pangea/controllers/pangea_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index fbb23845c..95a8ef57a 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -196,7 +196,7 @@ class PangeaController { return; } - const List botDMs = []; + final List botDMs = []; for (final room in matrixState.client.rooms) { if (await room.isBotDM) { botDMs.add(room); From aad699d6bdadb7f602ad741f132ce7a4f6383e8f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:21:50 -0400 Subject: [PATCH 015/115] better error handling for null content in practice activity fromJSON method --- .../practice_activity_model.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 55b171397..644031e47 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class ConstructIdentifier { final String lemma; @@ -186,8 +187,15 @@ class PracticeActivityModel { // moving from multiple_choice to content as the key // this is to make the model more generic // here for backward compatibility - final Map content = - (json['content'] ?? json["multiple_choice"]) as Map; + final Map? content = + (json['content'] ?? json["multiple_choice"]) as Map?; + + if (content == null) { + Sentry.addBreadcrumb( + Breadcrumb(data: {"json": json}), + ); + throw ("content is null in PracticeActivityModel.fromJson"); + } return PracticeActivityModel( tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs']) From a6d41f9c77b1e6732c93c0f8fece1093e886d64a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:36:55 -0400 Subject: [PATCH 016/115] check if mounted before clearing selected events, clear selected events on dispose --- lib/pages/chat/chat.dart | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index a51a86b37..d5cb30747 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -560,6 +560,7 @@ class ChatController extends State //#Pangea choreographer.stateListener.close(); choreographer.dispose(); + clearSelectedEvents(); MatrixState.pAnyState.closeOverlay(); //Pangea# super.dispose(); @@ -1334,13 +1335,18 @@ class ChatController extends State } // Pangea# - void clearSelectedEvents() => setState(() { - // #Pangea - closeSelectionOverlay(); - // Pangea# - selectedEvents.clear(); - showEmojiPicker = false; - }); + void clearSelectedEvents() { + // #Pangea + if (!mounted) return; + // Pangea# + setState(() { + // #Pangea + closeSelectionOverlay(); + // Pangea# + selectedEvents.clear(); + showEmojiPicker = false; + }); + } void clearSingleSelectedEvent() { if (selectedEvents.length <= 1) { From 8e0a807d4e56afcf41357b5f73ad7fc7ae72fcb5 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 09:45:06 -0400 Subject: [PATCH 017/115] removed UI logic for if bot settings is in new group page since it was removed --- .../conversation_bot_settings.dart | 134 ++---------------- 1 file changed, 15 insertions(+), 119 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 963949a78..7d94b6624 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -15,14 +15,12 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; class ConversationBotSettings extends StatefulWidget { - final Room? room; - final bool startOpen; + final Room room; final String? activeSpaceId; const ConversationBotSettings({ super.key, - this.room, - this.startOpen = false, + required this.room, this.activeSpaceId, }); @@ -32,10 +30,7 @@ class ConversationBotSettings extends StatefulWidget { class ConversationBotSettingsState extends State { late BotOptionsModel botOptions; - late bool isOpen; - late bool isCreating; bool addBot = false; - Room? parentSpace; ConversationBotSettingsState({Key? key}); @@ -49,19 +44,13 @@ class ConversationBotSettingsState extends State { @override void initState() { super.initState(); - isOpen = widget.startOpen; - botOptions = widget.room?.botOptions != null - ? BotOptionsModel.fromJson(widget.room?.botOptions?.toJson()) + botOptions = widget.room.botOptions != null + ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) : BotOptionsModel(); - widget.room?.botIsInRoom.then((bool isBotRoom) { - setState(() { - addBot = isBotRoom; - }); + + widget.room.botIsInRoom.then((bool isBotRoom) { + setState(() => addBot = isBotRoom); }); - parentSpace = widget.activeSpaceId != null - ? Matrix.of(context).client.getRoomById(widget.activeSpaceId!) - : null; - isCreating = widget.room == null; discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; discussionTopicController.text = botOptions.discussionTopic ?? ""; @@ -69,10 +58,9 @@ class ConversationBotSettingsState extends State { } Future setBotOption() async { - if (widget.room == null) return; try { await Matrix.of(context).client.setRoomStateWithKey( - widget.room!.id, + widget.room.id, PangeaEventTypes.botOptions, '', botOptions.toJson(), @@ -99,14 +87,13 @@ class ConversationBotSettingsState extends State { ); } - void updateAllBotOptions() { + void updateFromTextControllers() { botOptions.discussionTopic = discussionTopicController.text; botOptions.discussionKeywords = discussionKeywordsController.text; botOptions.customSystemPrompt = customSystemPromptController.text; } Future showBotOptionsDialog() async { - if (isCreating) return; final bool? confirm = await showDialog( context: context, builder: (BuildContext context) { @@ -141,65 +128,18 @@ class ConversationBotSettingsState extends State { ); if (confirm == true) { - updateAllBotOptions(); + updateFromTextControllers(); updateBotOption(() => botOptions = botOptions); - final bool isBotRoomMember = await widget.room?.botIsInRoom ?? false; + final bool isBotRoomMember = await widget.room.botIsInRoom; if (addBot && !isBotRoomMember) { - await widget.room?.invite(BotName.byEnvironment); + await widget.room.invite(BotName.byEnvironment); } else if (!addBot && isBotRoomMember) { - await widget.room?.kick(BotName.byEnvironment); + await widget.room.kick(BotName.byEnvironment); } } } - Future showNewRoomBotOptionsDialog() async { - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: addBot - ? Text( - L10n.of(context)!.addConversationBotButtonTitleRemove, - ) - : Text( - L10n.of(context)!.addConversationBotDialogTitleInvite, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(!addBot); - }, - child: addBot - ? Text( - L10n.of(context)! - .addConversationBotDialogRemoveConfirmation, - ) - : Text( - L10n.of(context)! - .addConversationBotDialogInviteConfirmation, - ), - ), - ], - ); - }, - ); - - if (confirm == true) { - setState(() => addBot = true); - widget.room?.invite(BotName.byEnvironment); - } else { - setState(() => addBot = false); - widget.room?.kick(BotName.byEnvironment); - } - } - final GlobalKey formKey = GlobalKey(); @override @@ -212,17 +152,12 @@ class ConversationBotSettingsState extends State { children: [ ListTile( title: Text( - isCreating - ? L10n.of(context)!.addConversationBot - : L10n.of(context)!.botConfig, + L10n.of(context)!.botConfig, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), - subtitle: isCreating - ? Text(L10n.of(context)!.addConversationBotDesc) - : null, leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, @@ -231,48 +166,9 @@ class ConversationBotSettingsState extends State { expression: BotExpression.idle, ), ), - trailing: isCreating - ? ElevatedButton( - onPressed: showNewRoomBotOptionsDialog, - child: Text( - addBot - ? L10n.of(context)!.addConversationBotButtonRemove - : L10n.of(context)!.addConversationBotButtonInvite, - ), - ) - : const Icon(Icons.settings), + trailing: const Icon(Icons.settings), onTap: showBotOptionsDialog, ), - if (isCreating && addBot) - Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - Form( - key: formKey, - child: ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: - discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: - customSystemPromptController, - ), - ), - ], - ), - ), ], ), ); From e1062b3443211f4d4bc4a4f1c6df0d581eaf76b6 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 10:54:46 -0400 Subject: [PATCH 018/115] if not add bot, show disabled, low opacity form. Moved state handling to top level widget --- .../conversation_bot_mode_dynamic_zone.dart | 27 +- .../conversation_bot_mode_select.dart | 8 +- .../conversation_bot_settings.dart | 332 +++++++++--------- .../conversation_bot_settings_form.dart | 68 ++-- .../space/language_level_dropdown.dart | 4 +- 5 files changed, 215 insertions(+), 224 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index dc1997a20..f001fe8f7 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -4,20 +4,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeDynamicZone extends StatelessWidget { - final BotOptionsModel initialBotOptions; - final GlobalKey formKey; - + final BotOptionsModel botOptions; final TextEditingController discussionTopicController; final TextEditingController discussionKeywordsController; final TextEditingController customSystemPromptController; + final bool enabled; + const ConversationBotModeDynamicZone({ super.key, - required this.initialBotOptions, - required this.formKey, + required this.botOptions, required this.discussionTopicController, required this.discussionKeywordsController, required this.customSystemPromptController, + this.enabled = true, }); @override @@ -29,9 +29,12 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionTopicPlaceholder, ), controller: discussionTopicController, - validator: (value) => value == null || value.isEmpty + validator: (value) => enabled && + botOptions.mode == BotMode.discussion && + (value == null || value.isEmpty) ? L10n.of(context)!.enterDiscussionTopic : null, + enabled: enabled, ), const SizedBox(height: 12), TextFormField( @@ -40,6 +43,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotDiscussionZone_discussionKeywordsPlaceholder, ), controller: discussionKeywordsController, + enabled: enabled, ), ]; @@ -49,17 +53,20 @@ class ConversationBotModeDynamicZone extends StatelessWidget { hintText: L10n.of(context)! .conversationBotCustomZone_customSystemPromptPlaceholder, ), - validator: (value) => value == null || value.isEmpty + validator: (value) => enabled && + botOptions.mode == BotMode.custom && + (value == null || value.isEmpty) ? L10n.of(context)!.enterPrompt : null, controller: customSystemPromptController, + enabled: enabled, ), ]; return Column( children: [ - if (initialBotOptions.mode == BotMode.discussion) ...discussionChildren, - if (initialBotOptions.mode == BotMode.custom) ...customChildren, + if (botOptions.mode == BotMode.discussion) ...discussionChildren, + if (botOptions.mode == BotMode.custom) ...customChildren, const SizedBox(height: 12), CheckboxListTile( title: Text( @@ -67,7 +74,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { .conversationBotCustomZone_customTriggerReactionEnabledLabel, ), enabled: false, - value: initialBotOptions.customTriggerReactionEnabled ?? true, + value: botOptions.customTriggerReactionEnabled ?? true, onChanged: null, ), const SizedBox(height: 12), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index c22801d25..3d893f078 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -4,12 +4,14 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotModeSelect extends StatelessWidget { final String? initialMode; - final void Function(String?)? onChanged; + final void Function(String?) onChanged; + final bool enabled; const ConversationBotModeSelect({ super.key, this.initialMode, - this.onChanged, + required this.onChanged, + this.enabled = true, }); @override @@ -52,7 +54,7 @@ class ConversationBotModeSelect extends StatelessWidget { ), ), ], - onChanged: onChanged, + onChanged: enabled ? onChanged : null, ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 7d94b6624..4ff52642f 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -1,5 +1,7 @@ import 'dart:developer'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; @@ -11,17 +13,14 @@ 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'; import 'package:matrix/matrix.dart'; class ConversationBotSettings extends StatefulWidget { final Room room; - final String? activeSpaceId; const ConversationBotSettings({ super.key, required this.room, - this.activeSpaceId, }); @override @@ -29,35 +28,7 @@ class ConversationBotSettings extends StatefulWidget { } class ConversationBotSettingsState extends State { - late BotOptionsModel botOptions; - bool addBot = false; - - ConversationBotSettingsState({Key? key}); - - final TextEditingController discussionTopicController = - TextEditingController(); - final TextEditingController discussionKeywordsController = - TextEditingController(); - final TextEditingController customSystemPromptController = - TextEditingController(); - - @override - void initState() { - super.initState(); - botOptions = widget.room.botOptions != null - ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) - : BotOptionsModel(); - - widget.room.botIsInRoom.then((bool isBotRoom) { - setState(() => addBot = isBotRoom); - }); - - discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; - discussionTopicController.text = botOptions.discussionTopic ?? ""; - customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; - } - - Future setBotOption() async { + Future setBotOptions(BotOptionsModel botOptions) async { try { await Matrix.of(context).client.setRoomStateWithKey( widget.room.id, @@ -71,77 +42,18 @@ class ConversationBotSettingsState extends State { } } - Future updateBotOption(void Function() makeLocalChange) async { - makeLocalChange(); - await showFutureLoadingDialog( - context: context, - future: () async { - try { - await setBotOption(); - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } - setState(() {}); - }, - ); - } - - void updateFromTextControllers() { - botOptions.discussionTopic = discussionTopicController.text; - botOptions.discussionKeywords = discussionKeywordsController.text; - botOptions.customSystemPrompt = customSystemPromptController.text; - } - Future showBotOptionsDialog() async { - final bool? confirm = await showDialog( + final BotOptionsModel? newBotOptions = await showDialog( context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) => Dialog( - child: Form( - key: formKey, - child: Container( - padding: const EdgeInsets.all(16), - constraints: const BoxConstraints( - maxWidth: 450, - maxHeight: 725, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: ConversationBotSettingsDialog( - addBot: addBot, - botOptions: botOptions, - formKey: formKey, - updateAddBot: (bool value) => - setState(() => addBot = value), - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, - ), - ), - ), - ), - ), - ); - }, + builder: (BuildContext context) => + ConversationBotSettingsDialog(room: widget.room), ); - if (confirm == true) { - updateFromTextControllers(); - updateBotOption(() => botOptions = botOptions); - - final bool isBotRoomMember = await widget.room.botIsInRoom; - if (addBot && !isBotRoomMember) { - await widget.room.invite(BotName.byEnvironment); - } else if (!addBot && isBotRoomMember) { - await widget.room.kick(BotName.byEnvironment); - } + if (newBotOptions != null) { + setBotOptions(newBotOptions); } } - final GlobalKey formKey = GlobalKey(); - @override Widget build(BuildContext context) { return AnimatedContainer( @@ -175,91 +87,175 @@ class ConversationBotSettingsState extends State { } } -class ConversationBotSettingsDialog extends StatelessWidget { - final bool addBot; - final BotOptionsModel botOptions; - final GlobalKey formKey; - - final void Function(bool) updateAddBot; - - final TextEditingController discussionTopicController; - final TextEditingController discussionKeywordsController; - final TextEditingController customSystemPromptController; +class ConversationBotSettingsDialog extends StatefulWidget { + final Room room; const ConversationBotSettingsDialog({ super.key, - required this.addBot, - required this.botOptions, - required this.formKey, - required this.updateAddBot, - required this.discussionTopicController, - required this.discussionKeywordsController, - required this.customSystemPromptController, + required this.room, }); + @override + ConversationBotSettingsDialogState createState() => + ConversationBotSettingsDialogState(); +} + +class ConversationBotSettingsDialogState + extends State { + late BotOptionsModel botOptions; + bool addBot = false; + + final TextEditingController discussionTopicController = + TextEditingController(); + final TextEditingController discussionKeywordsController = + TextEditingController(); + final TextEditingController customSystemPromptController = + TextEditingController(); + + @override + void initState() { + super.initState(); + botOptions = widget.room.botOptions != null + ? BotOptionsModel.fromJson(widget.room.botOptions?.toJson()) + : BotOptionsModel(); + + widget.room.botIsInRoom.then((bool isBotRoom) { + setState(() => addBot = isBotRoom); + }); + + discussionKeywordsController.text = botOptions.discussionKeywords ?? ""; + discussionTopicController.text = botOptions.discussionTopic ?? ""; + customSystemPromptController.text = botOptions.customSystemPrompt ?? ""; + } + + final GlobalKey formKey = GlobalKey(); + + void updateFromTextControllers() { + botOptions.discussionTopic = discussionTopicController.text; + botOptions.discussionKeywords = discussionKeywordsController.text; + botOptions.customSystemPrompt = customSystemPromptController.text; + } + + void onUpdateChatMode(String? mode) { + setState(() => botOptions.mode = mode ?? BotMode.discussion); + } + + void onUpdateBotLanguage(String? language) { + setState(() => botOptions.targetLanguage = language); + } + + void onUpdateBotVoice(String? voice) { + setState(() => botOptions.targetVoice = voice); + } + + void onUpdateBotLanguageLevel(int? level) { + setState(() => botOptions.languageLevel = level); + } + @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - SwitchListTile( - title: Text( - L10n.of(context)!.conversationBotStatus, - ), - value: addBot, - onChanged: updateAddBot, - contentPadding: const EdgeInsets.all(4), - ), - if (addBot) - Expanded( - child: SingleChildScrollView( - child: Column( + final dialogContent = Form( + key: formKey, + child: Container( + padding: const EdgeInsets.all(16), + constraints: kIsWeb + ? const BoxConstraints( + maxWidth: 450, + maxHeight: 725, + ) + : null, + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SwitchListTile( + title: Text( + L10n.of(context)!.conversationBotStatus, + ), + value: addBot, + onChanged: (bool value) { + setState(() => addBot = value); + }, + contentPadding: const EdgeInsets.all(4), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 20), + AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: addBot ? 1.0 : 0.5, + child: ConversationBotSettingsForm( + botOptions: botOptions, + discussionKeywordsController: + discussionKeywordsController, + discussionTopicController: discussionTopicController, + customSystemPromptController: + customSystemPromptController, + enabled: addBot, + onUpdateBotMode: onUpdateChatMode, + onUpdateBotLanguage: onUpdateBotLanguage, + onUpdateBotVoice: onUpdateBotVoice, + onUpdateBotLanguageLevel: onUpdateBotLanguageLevel, + ), + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - const SizedBox(height: 20), - ConversationBotSettingsForm( - botOptions: botOptions, - formKey: formKey, - discussionKeywordsController: discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: customSystemPromptController, + TextButton( + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(L10n.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: () async { + final isValid = formKey.currentState!.validate(); + if (!isValid) return; + + updateFromTextControllers(); + + final bool isBotRoomMember = + await widget.room.botIsInRoom; + if (addBot && !isBotRoomMember) { + await widget.room.invite(BotName.byEnvironment); + } else if (!addBot && isBotRoomMember) { + await widget.room.kick(BotName.byEnvironment); + } + + Navigator.of(context).pop(botOptions); + }, + child: Text(L10n.of(context)!.confirm), ), ], ), - ), + ], ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(L10n.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: () { - final isValid = formKey.currentState!.validate(); - if (!isValid) return; - Navigator.of(context).pop(true); - }, - child: Text(L10n.of(context)!.confirm), - ), - ], ), - ], + ), ); + + return kIsWeb + ? Dialog(child: dialogContent) + : Dialog.fullscreen(child: dialogContent); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index 5447b67bb..195d35801 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart'; @@ -7,38 +6,32 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class ConversationBotSettingsForm extends StatefulWidget { +class ConversationBotSettingsForm extends StatelessWidget { final BotOptionsModel botOptions; - final GlobalKey formKey; final TextEditingController discussionTopicController; final TextEditingController discussionKeywordsController; final TextEditingController customSystemPromptController; + final bool enabled; + final void Function(String?) onUpdateBotMode; + final void Function(String?) onUpdateBotLanguage; + final void Function(String?) onUpdateBotVoice; + final void Function(int?) onUpdateBotLanguageLevel; + const ConversationBotSettingsForm({ super.key, required this.botOptions, - required this.formKey, required this.discussionTopicController, required this.discussionKeywordsController, required this.customSystemPromptController, + required this.onUpdateBotMode, + required this.onUpdateBotLanguage, + required this.onUpdateBotVoice, + required this.onUpdateBotLanguageLevel, + this.enabled = true, }); - @override - ConversationBotSettingsFormState createState() => - ConversationBotSettingsFormState(); -} - -class ConversationBotSettingsFormState - extends State { - late BotOptionsModel botOptions; - - @override - void initState() { - super.initState(); - botOptions = widget.botOptions; - } - @override Widget build(BuildContext context) { return Column( @@ -64,9 +57,7 @@ class ConversationBotSettingsFormState ), ); }).toList(), - onChanged: (String? newValue) => { - setState(() => botOptions.targetLanguage = newValue!), - }, + onChanged: enabled ? onUpdateBotLanguage : null, ), const SizedBox(height: 12), DropdownButtonFormField( @@ -80,20 +71,16 @@ class ConversationBotSettingsFormState isExpanded: true, icon: const Icon(Icons.keyboard_arrow_down), items: const [], - onChanged: (String? newValue) => { - setState(() => botOptions.targetVoice = newValue!), - }, + onChanged: enabled ? onUpdateBotVoice : null, ), const SizedBox(height: 12), LanguageLevelDropdown( initialLevel: botOptions.languageLevel, - onChanged: (int? newValue) => { - setState(() { - botOptions.languageLevel = newValue!; - }), - }, - validator: (value) => - value == null ? L10n.of(context)!.enterLanguageLevel : null, + onChanged: onUpdateBotLanguageLevel, + validator: (value) => enabled && value == null + ? L10n.of(context)!.enterLanguageLevel + : null, + enabled: enabled, ), const SizedBox(height: 12), Align( @@ -108,19 +95,16 @@ class ConversationBotSettingsFormState ), ConversationBotModeSelect( initialMode: botOptions.mode, - onChanged: (String? mode) => { - setState(() { - botOptions.mode = mode ?? BotMode.discussion; - }), - }, + onChanged: onUpdateBotMode, + enabled: enabled, ), const SizedBox(height: 12), ConversationBotModeDynamicZone( - initialBotOptions: botOptions, - discussionTopicController: widget.discussionTopicController, - discussionKeywordsController: widget.discussionKeywordsController, - customSystemPromptController: widget.customSystemPromptController, - formKey: widget.formKey, + botOptions: botOptions, + discussionTopicController: discussionTopicController, + discussionKeywordsController: discussionKeywordsController, + customSystemPromptController: customSystemPromptController, + enabled: enabled, ), ], ); diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index 0964a49ab..0b238485a 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -7,12 +7,14 @@ class LanguageLevelDropdown extends StatelessWidget { final int? initialLevel; final void Function(int?)? onChanged; final String? Function(int?)? validator; + final bool enabled; const LanguageLevelDropdown({ super.key, this.initialLevel, this.onChanged, this.validator, + this.enabled = true, }); @override @@ -42,7 +44,7 @@ class LanguageLevelDropdown extends StatelessWidget { ), ); }).toList(), - onChanged: onChanged, + onChanged: enabled ? onChanged : null, validator: validator, ); } From b3a0ad1b6e14aa8ea7770d6e089f3cf668c59175 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 11:09:25 -0400 Subject: [PATCH 019/115] added x button to coversation bot settings and make text fields expand to fit contents --- .../conversation_bot_mode_dynamic_zone.dart | 9 +++++++ .../conversation_bot_settings.dart | 25 ++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index f001fe8f7..e8f33fe3d 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -35,6 +35,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { ? L10n.of(context)!.enterDiscussionTopic : null, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), const SizedBox(height: 12), TextFormField( @@ -44,6 +47,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { ), controller: discussionKeywordsController, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), ]; @@ -60,6 +66,9 @@ class ConversationBotModeDynamicZone extends StatelessWidget { : null, controller: customSystemPromptController, enabled: enabled, + minLines: 1, // Minimum number of lines + maxLines: null, // Allow the field to expand based on content + keyboardType: TextInputType.multiline, ), ]; diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 4ff52642f..833ba8924 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -169,17 +169,24 @@ class ConversationBotSettingsDialogState child: Column( mainAxisSize: MainAxisSize.min, children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + L10n.of(context)!.botConfig, + style: Theme.of(context).textTheme.titleLarge, + ), ), - child: Text( - L10n.of(context)!.botConfig, - style: Theme.of(context).textTheme.titleLarge, + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(null), ), - ), + ], ), SwitchListTile( title: Text( From f1ac5709f7c56c2b79fdf328920e2725a19bb2be Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 14:44:26 -0400 Subject: [PATCH 020/115] adjustment to layouts of toolbar content to make dynamic sizing work --- assets/l10n/intl_en.arb | 12 ++-- .../widgets/chat/message_audio_card.dart | 46 +++++++------ .../chat/message_speech_to_text_card.dart | 18 ++--- lib/pangea/widgets/chat/message_toolbar.dart | 60 +++++----------- .../chat/message_translation_card.dart | 62 +++++++++-------- .../widgets/common/icon_number_widget.dart | 2 +- .../practice_activity_card.dart | 69 +++++++++---------- lib/pangea/widgets/select_to_define.dart | 22 +++--- 8 files changed, 134 insertions(+), 157 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ffdcadc2d..51ab9d1d7 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4114,14 +4114,14 @@ "placeholders": {} }, "addChatToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.", - "addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space''s chat list.", + "addSpaceToSpaceDesc": "Adding a sub space to space will make the sub space appear in the main space's chat list.", "spaceAnalytics": "Space Analytics", "changeAnalyticsLanguage": "Change Analytics Language", "suggestToSpace": "Suggest this space", - "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space''s chat list", + "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list", "practice": "Practice", "noLanguagesSet": "No languages set", - "noActivitiesFound": "That''s enough on this for now! Come back later for more.", + "noActivitiesFound": "That's enough on this for now! Come back later for more.", "hintTitle": "Hint:", "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "previous": "Previous", @@ -4225,14 +4225,14 @@ "discoverHomeservers": "Discover homeservers", "whatIsAHomeserver": "What is a homeserver?", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", - "doesNotSeemToBeAValidHomeserver": "Doesn''t seem to be a compatible homeserver. Wrong URL?", + "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?", "grammar": "Grammar", "contactHasBeenInvitedToTheChat": "Contact has been invited to the chat", "inviteChat": "📨 Invite chat", "chatName": "Chat name", "reportContentIssueTitle": "Report content issue", "feedback": "Optional feedback", - "reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we''ll try again.", + "reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.", "clickTheWordAgainToDeselect": "Click the selected word to deselect it.", "l2SupportNa": "Not Available", "l2SupportAlpha": "Alpha", @@ -4355,7 +4355,7 @@ "grammarCopyNumber": "Number", "grammarCopyConjType": "Conjunction Type", "grammarCopyPolarity": "Polarity", - "grammarCopyNumberPsor": "Possessor''s Number", + "grammarCopyNumberPsor": "Possessor's Number", "grammarCopyCase": "Case", "grammarCopyDefinite": "Definiteness", "grammarCopyNumForm": "Numeral Form", diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 9250aa2c4..64bb449aa 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -187,26 +187,32 @@ class MessageAudioCardState extends State { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, - child: _isLoading - ? const ToolbarContentLoadingIndicator() - : audioFile != null - ? Column( - children: [ - AudioPlayerWidget( - null, - matrixFile: audioFile, - sectionStartMS: sectionStartMS, - sectionEndMS: sectionEndMS, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - tts.missingVoiceButton, - ], - ) - : const CardErrorWidget(), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: minCardHeight), + alignment: Alignment.center, + child: _isLoading + ? const ToolbarContentLoadingIndicator() + : audioFile != null + ? Column( + children: [ + AudioPlayerWidget( + null, + matrixFile: audioFile, + sectionStartMS: sectionStartMS, + sectionEndMS: sectionEndMS, + color: + Theme.of(context).colorScheme.onPrimaryContainer, + ), + tts.missingVoiceButton, + ], + ) + : const CardErrorWidget(), + ), + ], ); } } diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index 012647b5a..92489ce29 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; @@ -154,16 +153,11 @@ class MessageSpeechToTextCardState extends State { return CardErrorWidget(error: error); } - final int words = speechToTextResponse!.transcript.sttTokens.length; - final int accuracy = speechToTextResponse!.transcript.confidence; - final int total = words * accuracy; - //TODO: find better icons - return Container( + return Padding( padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, child: Column( + mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), RichText( @@ -171,19 +165,15 @@ class MessageSpeechToTextCardState extends State { ), const SizedBox(height: 16), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.min, children: [ - // IconNumberWidget( - // icon: Icons.abc, - // number: (selectedToken == null ? words : 1).toString(), - // toolTip: L10n.of(context)!.words, - // ), IconNumberWidget( icon: Symbols.target, number: "${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%", toolTip: L10n.of(context)!.accuracy, ), + const SizedBox(width: 16), IconNumberWidget( icon: Icons.speed, number: wordsPerMinuteString != null diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index b204ebe54..bcffbdf2a 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -101,47 +101,25 @@ class MessageToolbar extends StatelessWidget { @override Widget build(BuildContext context) { - return Material( - key: MatrixState.pAnyState - .layerLinkAndKey('${pangeaMessageEvent.eventId}-toolbar') - .key, - type: MaterialType.transparency, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - width: 2, - color: - Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), - ), - ), - constraints: const BoxConstraints( - maxHeight: AppConfig.toolbarMaxHeight, - ), - // child: Row( - // children: [ - // Expanded( - child: SingleChildScrollView( - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: toolbarContent, - ), - ), - // ), - // ], - // ), - ), - ], - ), - ], + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary.withOpacity(0.5), + ), + borderRadius: const BorderRadius.all( + Radius.circular(AppConfig.borderRadius), + ), + ), + constraints: const BoxConstraints( + maxHeight: AppConfig.toolbarMaxHeight, + ), + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: toolbarContent, + ), ), ); } diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 5e66d9966..c209e7b8f 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -134,36 +133,41 @@ class MessageTranslationCardState extends State { return const CardErrorWidget(); } - return Container( + return Padding( padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, - child: _fetchingTranslation - ? const ToolbarContentLoadingIndicator() - : Column( - children: [ - widget.selection != null - ? Text( - selectionTranslation!, - style: BotStyle.text(context), - ) - : Text( - repEvent!.text, - style: BotStyle.text(context), - ), - if (notGoingToTranslate && widget.selection == null) - InlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - onClose: () => setState(() {}), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _fetchingTranslation + ? const ToolbarContentLoadingIndicator() + : Flexible( + child: Column( + children: [ + widget.selection != null + ? Text( + selectionTranslation!, + style: BotStyle.text(context), + ) + : Text( + repEvent!.text, + style: BotStyle.text(context), + ), + if (notGoingToTranslate && widget.selection == null) + InlineTooltip( + instructionsEnum: InstructionsEnum.l1Translation, + onClose: () => setState(() {}), + ), + if (widget.selection != null) + InlineTooltip( + instructionsEnum: + InstructionsEnum.clickAgainToDeselect, + onClose: () => setState(() {}), + ), + ], ), - if (widget.selection != null) - InlineTooltip( - instructionsEnum: InstructionsEnum.clickAgainToDeselect, - onClose: () => setState(() {}), - ), - // if (widget.selection != null) - ], - ), + ), + ], + ), ); } } diff --git a/lib/pangea/widgets/common/icon_number_widget.dart b/lib/pangea/widgets/common/icon_number_widget.dart index 24307112c..099baf3df 100644 --- a/lib/pangea/widgets/common/icon_number_widget.dart +++ b/lib/pangea/widgets/common/icon_number_widget.dart @@ -30,7 +30,7 @@ class IconNumberWidget extends StatelessWidget { ), onPressed: onPressed, ), - const SizedBox(width: 8), + const SizedBox(width: 5), Text( number.toString(), style: TextStyle( diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 8ac0664b3..277a8bed9 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -14,7 +14,6 @@ import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/content_issue_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; @@ -314,45 +313,43 @@ class MessagePracticeActivityCardState extends State { return GamifiedTextWidget(userMessage: userMessage!); } - return Container( - constraints: const BoxConstraints( - maxWidth: 350, - minWidth: 350, - minHeight: minCardHeight, - ), - child: Stack( - alignment: Alignment.center, - children: [ - // Main content - const Positioned( - child: PointsGainedAnimation(), - ), - Container( - padding: const EdgeInsets.all(8), - child: activityWidget, - ), - // Conditionally show the darkening and progress indicator based on the loading state - if (!savoringTheJoy && fetchingActivity) ...[ - // Semi-transparent overlay + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + // Main content + const Positioned( + child: PointsGainedAnimation(), + ), Container( - color: Colors.black.withOpacity(0.5), // Darkening effect + padding: const EdgeInsets.all(8), + child: activityWidget, ), - // Circular progress indicator in the center - const Center( - child: CircularProgressIndicator(), + // Conditionally show the darkening and progress indicator based on the loading state + if (!savoringTheJoy && fetchingActivity) ...[ + // Semi-transparent overlay + Container( + color: Colors.black.withOpacity(0.5), // Darkening effect + ), + // Circular progress indicator in the center + const Center( + child: CircularProgressIndicator(), + ), + ], + // Flag button in the top right corner + Positioned( + top: 0, + right: 0, + child: ContentIssueButton( + isActive: currentActivity != null, + submitFeedback: submitFeedback, + ), ), ], - // Flag button in the top right corner - Positioned( - top: 0, - right: 0, - child: ContentIssueButton( - isActive: currentActivity != null, - submitFeedback: submitFeedback, - ), - ), - ], - ), + ), + ], ); } } diff --git a/lib/pangea/widgets/select_to_define.dart b/lib/pangea/widgets/select_to_define.dart index 7020e5e77..556cc7f94 100644 --- a/lib/pangea/widgets/select_to_define.dart +++ b/lib/pangea/widgets/select_to_define.dart @@ -1,5 +1,4 @@ import 'package:fluffychat/pangea/utils/bot_style.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -10,16 +9,19 @@ class SelectToDefine extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Container( - constraints: const BoxConstraints(minHeight: minCardHeight), - padding: const EdgeInsets.all(8), - child: Center( - child: Text( - L10n.of(context)!.selectToDefine, - style: BotStyle.text(context), + return Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + L10n.of(context)!.selectToDefine, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), ), - ), + ], ), ); } From 985ccfe30d0e0c613408681fcffd4c69aa6d845c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 14:54:42 -0400 Subject: [PATCH 021/115] better error handling if originalSent is null in _fetchActivity --- .../practice_activity_card.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 277a8bed9..82d3c0a17 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -4,7 +4,6 @@ import 'dart:developer'; 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'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; @@ -119,13 +118,25 @@ class MessagePracticeActivityCardState extends State { return null; } + if (widget.pangeaMessageEvent.originalSent == null) { + debugger(when: kDebugMode); + _updateFetchingActivity(false); + ErrorHandler.logError( + e: Exception('No original message found in _fetchNewActivity'), + data: { + 'event': widget.pangeaMessageEvent.event.toJson(), + }, + ); + return null; + } + final PracticeActivityModel? ourNewActivity = await pangeaController .practiceGenerationController .getPracticeActivity( MessageActivityRequest( userL1: pangeaController.languageController.userL1!.langCode, userL2: pangeaController.languageController.userL2!.langCode, - messageText: representation!.text, + messageText: widget.pangeaMessageEvent.originalSent!.text, tokensWithXP: await targetTokensController.targetTokens( context, widget.pangeaMessageEvent, @@ -256,11 +267,6 @@ class MessagePracticeActivityCardState extends State { }); } - RepresentationEvent? get representation => - widget.pangeaMessageEvent.originalSent; - - String get messsageText => representation!.text; - PangeaController get pangeaController => MatrixState.pangeaController; /// The widget that displays the current activity. From c028f64f606c46d6a99edfe60b64ebf0ad17770f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 15:01:07 -0400 Subject: [PATCH 022/115] tts web version always return null on stop(), don't log error --- lib/pangea/widgets/chat/tts_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 7e7381c53..225d7a04e 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -96,7 +96,7 @@ class TtsController { // return type is dynamic but apparent its supposed to be 1 // https://pub.dev/packages/flutter_tts - if (result != 1) { + if (result != 1 && !kIsWeb) { ErrorHandler.logError( m: 'Unexpected result from tts.speak', data: { From b499ef2f14d1eabb7ec5ce93ecc66c4ceb60ee8c Mon Sep 17 00:00:00 2001 From: choreo development Date: Tue, 22 Oct 2024 15:05:38 -0400 Subject: [PATCH 023/115] finishedd copying to intl_es.arb --- assets/l10n/intl_es.arb | 142 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index dd8312d8f..acfcdee46 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4731,5 +4731,145 @@ } }, "commandHint_googly": "Enviar unos ojos saltones", - "reportContentIssue": "Problema de contenido" + "reportContentIssue": "Problema de contenido", + "alwaysUse24HourFormat": "falso", + "countChatsAndCountParticipants": "{chats} chats y {participants} participantes", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "No se encontraron más chats...", + "noChatsFoundHere": "Aún no se encontraron chats aquí. Inicia un nuevo chat con alguien usando el botón de abajo. ⤵️", + "joinedChats": "Chats unidos", + "unread": "No leído", + "space": "Espacio", + "spaces": "Espacios", + "enterASpacepName": "Ingresa un nombre", + "invitedBy": "📩 Invitado por {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "clickMessageBody": "Haz clic en un mensaje para herramientas de idioma como traducción, reproducción y más!", + "searchIn": "Buscar en el chat \"{chat}\"...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "subscribedToUnlockTools": "¡Suscríbete para desbloquear la traducción interactiva y la verificación gramatical, la reproducción de audio, las actividades de práctica personalizadas y la analítica de aprendizaje!", + "conversationBotModeSelectOption_storyGame": "Juego de Historia", + "conversationBotCustomZone_title": "Configuraciones Personalizadas", + "conversationBotCustomZone_customSystemPromptLabel": "Mensaje del sistema", + "conversationBotCustomZone_customSystemPromptPlaceholder": "Establecer mensaje del sistema personalizado", + "conversationBotCustomZone_customSystemPromptEmptyError": "Falta mensaje del sistema personalizado", + "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responde a la reacción ⏩", + "botConfig": "Configuraciones del Bot de Conversación", + "addConversationBotDialogTitleInvite": "Confirmar la invitación del bot de conversación", + "addConversationBotButtonInvite": "Invitar", + "addConversationBotDialogInviteConfirmation": "Invitar", + "addConversationBotButtonTitleRemove": "Confirmar la eliminación del bot de conversación", + "addConversationBotButtonRemove": "Eliminar", + "addConversationBotDialogRemoveConfirmation": "Eliminar", + "conversationBotConfigConfirmChange": "Confirmar", + "conversationBotStatus": "Estado del Bot", + "conversationBotTextAdventureZone_title": "Aventura de Texto", + "conversationBotTextAdventureZone_instructionLabel": "Instrucciones del Maestro del Juego", + "conversationBotTextAdventureZone_instructionPlaceholder": "Establecer instrucciones del maestro del juego", + "conversationBotCustomZone_instructionSystemPromptEmptyError": "Faltan instrucciones del maestro del juego", + "suggestToSpace": "Sugerir este espacio", + "suggestToSpaceDesc": "Los subespacios sugeridos aparecerán en la lista de chats de su espacio principal", + "practice": "Práctica", + "noLanguagesSet": "No hay idiomas configurados", + "hintTitle": "Sugerencia:", + "speechToTextBody": "Ve qué tan bien lo hiciste al mirar tus puntajes de Precisión y Palabras Por Minuto.", + "previous": "Anterior", + "languageButtonLabel": "Idioma: {currentLanguage}", + "@languageButtonLabel": { + "type": "text", + "placeholders": { + "currentLanguage": {} + } + }, + "changeAnalyticsView": "Cambiar Vista de Análisis", + "l1TranslationBody": "Los mensajes en tu idioma base no serán traducidos.", + "continueText": "Continuar", + "deleteSubscriptionWarningTitle": "YTienes una suscripción activa", + "deleteSubscriptionWarningBody": "Eliminar tu cuenta no cancelará automáticamente tu suscripción.", + "manageSubscription": "Gestionar Suscripción", + "createSpace": "Crear espacio", + "createChat": "Crear chat", + "error520Title": "Por favor, intenta de nuevo.", + "error520Desc": "Lo sentimos, no pudimos entender tu mensaje...", + "wordsUsed": "Palabras Usadas", + "errorTypes": "Tipos de Error", + "level": "Nivel", + "canceledSend": "Envío cancelado", + "morphsUsed": "Morphs Usados", + "translationChoicesBody": "Haz clic y mantén presionada una opción para una pista.", + "sendCanceled": "Envío cancelado", + "goToSpace": "Ir al espacio: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "Marcar como no leído", + "userLevel": "{level} - Usuario", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "moderatorLevel": "{level} - Moderador", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Administrador", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "Cambiar la configuración general del chat.", + "inviteOtherUsers": "Invitar a otros usuarios a este chat", + "changeTheChatPermissions": "Cambiar los permisos del chat", + "changeTheVisibilityOfChatHistory": "Cambiar la visibilidad del historial de chat", + "changeTheCanonicalRoomAlias": "Cambiar la dirección del chat público principal.", + "sendRoomNotifications": "Enviar una notificación a @room", + "changeTheDescriptionOfTheGroup": "Cambiar la descripción del chat", + "chatPermissionsDescription": "Define qué nivel de poder es necesario para ciertas acciones en este chat. Los niveles de poder 0, 50 y 100 suelen representar a usuarios, moderadores y administradores, pero cualquier graduación es posible.", + "updateInstalled": "🎉 ¡Actualización {version} instalada!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "loginWithMatrixId": "Iniciar sesión con Matrix-ID.", + "discoverHomeservers": "Descubrir homeservers", + "whatIsAHomeserver": "¿Qué es un homeserver?", + "homeserverDescription": "Todos tus datos se almacenan en el homeserver, al igual que un proveedor de correo electrónico. Puedes elegir qué homeserver deseas utilizar, mientras que aún puedes comunicarte con todos. Aprende más en https://matrix.org.", + "doesNotSeemToBeAValidHomeserver": "No parece ser un homeserver compatible. ¿URL incorrecta?", + "grammar": "Gramática", + "contactHasBeenInvitedToTheChat": "El contacto ha sido invitado al chat", + "inviteChat": "📨 Invitar al chat", + "chatName": "Nombre del chat", + "reportContentIssueTitle": "Informar sobre un problema de contenido", + "feedback": "Comentarios opcionales", + "reportContentIssueDescription": "¡Ups! La IA puede facilitar experiencias de aprendizaje personalizadas, pero... también alucina. Por favor, proporciona cualquier comentario que tengas y lo intentaremos de nuevo.", + "clickTheWordAgainToDeselect": "Click the selected word to deselect it.", + "l2SupportNa": "Haz clic en la palabra seleccionada para deseleccionarla", + "l2SupportAlpha": "Alfa", + "l2SupportBeta": "Beta", + "l2SupportFull": "Lleno" } \ No newline at end of file From 2a7fd9a9620b27e3f4161601e19e7025368ce49a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 15:27:01 -0400 Subject: [PATCH 024/115] always provice error message to card error widget --- .../widgets/chat/message_audio_card.dart | 4 +- .../chat/message_translation_card.dart | 2 +- lib/pangea/widgets/igc/card_error_widget.dart | 39 ++++++++----------- lib/pangea/widgets/igc/card_header.dart | 17 ++++---- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 64bb449aa..82375bacf 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -210,7 +210,9 @@ class MessageAudioCardState extends State { tts.missingVoiceButton, ], ) - : const CardErrorWidget(), + : const CardErrorWidget( + error: "Null audio file in message_audio_card", + ), ), ], ); diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index c209e7b8f..81ed7a2e2 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -130,7 +130,7 @@ class MessageTranslationCardState extends State { if (!_fetchingTranslation && repEvent == null && selectionTranslation == null) { - return const CardErrorWidget(); + return const CardErrorWidget(error: "No translation found"); } return Padding( diff --git a/lib/pangea/widgets/igc/card_error_widget.dart b/lib/pangea/widgets/igc/card_error_widget.dart index 3f08f6277..708fdd88f 100644 --- a/lib/pangea/widgets/igc/card_error_widget.dart +++ b/lib/pangea/widgets/igc/card_error_widget.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; import 'package:fluffychat/pangea/widgets/igc/card_header.dart'; import 'package:flutter/material.dart'; @@ -21,30 +20,26 @@ class CardErrorWidget extends StatelessWidget { Widget build(BuildContext context) { final ErrorCopy errorCopy = ErrorCopy(context, error); - return Container( + return Padding( padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CardHeader( - text: errorCopy.title, - botExpression: BotExpression.addled, - onClose: () => choreographer?.onMatchError( - cursorOffset: offset, - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardHeader( + text: errorCopy.title, + botExpression: BotExpression.addled, + onClose: () => choreographer?.onMatchError( + cursorOffset: offset, ), - const SizedBox(height: 10.0), - Center( - child: Text( - errorCopy.body, - style: BotStyle.text(context), - ), + ), + const SizedBox(height: 10.0), + Center( + child: Text( + errorCopy.body, + style: BotStyle.text(context), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pangea/widgets/igc/card_header.dart b/lib/pangea/widgets/igc/card_header.dart index 5ee6b98f2..671e58492 100644 --- a/lib/pangea/widgets/igc/card_header.dart +++ b/lib/pangea/widgets/igc/card_header.dart @@ -1,8 +1,8 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; import '../../../widgets/matrix.dart'; -import '../../utils/bot_style.dart'; import '../common/bot_face_svg.dart'; class CardHeader extends StatelessWidget { @@ -22,8 +22,6 @@ class CardHeader extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 5.0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(top: 3.0), @@ -33,13 +31,12 @@ class CardHeader extends StatelessWidget { ), ), const SizedBox(width: 5.0), - Expanded( - child: Text( - text, - style: BotStyle.text(context), - textAlign: TextAlign.left, - ), + Text( + text, + style: BotStyle.text(context), + textAlign: TextAlign.left, ), + const SizedBox(width: 5.0), CircleAvatar( backgroundColor: AppConfig.primaryColor.withOpacity(0.1), child: IconButton( From f99b48af980fe35bb263f48f31e1e5656f1cb6f2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 15:32:41 -0400 Subject: [PATCH 025/115] if eventID is invalid, don't try to set read marker --- lib/pages/chat/chat.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d5cb30747..cb1c86f5e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -485,6 +485,14 @@ class ChatController extends State Future? setReadMarkerFuture; void setReadMarker({String? eventId}) { + // #Pangea + if (eventId != null && + (eventId.contains("web") || + eventId.contains("android") || + eventId.contains("ios"))) { + return; + } + // Pangea# if (setReadMarkerFuture != null) return; if (_scrolledUp) return; if (scrollUpBannerEventId != null) return; From 2d9cb5f8d989f4066fe6a03cf4218adce2585b52 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 15:38:56 -0400 Subject: [PATCH 026/115] clearer error messages on fail to invite bot to space --- lib/pages/new_space/new_space.dart | 3 ++- lib/pangea/controllers/pangea_controller.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index e89159900..9264d0657 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -136,7 +136,8 @@ class NewSpaceController extends State { await room.invite(BotName.byEnvironment); } catch (err) { ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${room.id}", + e: "Failed to invite pangea bot to new space", + data: {"spaceId": spaceId, "error": err}, ); } MatrixState.pangeaController.classController diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 95a8ef57a..3f0938052 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -298,7 +298,8 @@ class PangeaController { await space.invite(BotName.byEnvironment); } catch (err) { ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${space.id}", + e: "Failed to invite pangea bot to existing space", + data: {"spaceId": space.id, "error": err}, ); } } From d26e71123e84904246eedb06b67905bfe623ecef Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 22 Oct 2024 16:40:44 -0400 Subject: [PATCH 027/115] adding user l1 and l2 to text to speech req --- lib/pangea/controllers/text_to_speech_controller.dart | 6 ++++++ lib/pangea/matrix_event_wrappers/pangea_message_event.dart | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index e032c4045..9d409c515 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -80,17 +80,23 @@ class TTSToken { class TextToSpeechRequest { String text; String langCode; + String userL1; + String userL2; List tokens; TextToSpeechRequest({ required this.text, required this.langCode, + required this.userL1, + required this.userL2, required this.tokens, }); Map toJson() => { ModelKey.text: text, ModelKey.langCode: langCode, + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, ModelKey.tokens: tokens.map((token) => token.toJson()).toList(), }; diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 324c4a018..85dfb8760 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -93,6 +93,8 @@ class PangeaMessageEvent { text: rep.content.text, tokens: (await rep.tokensGlobal(context)).map((t) => t.text).toList(), langCode: langCode, + userL1: l1Code ?? LanguageKeys.unknownLanguage, + userL2: l2Code ?? LanguageKeys.unknownLanguage, ); final TextToSpeechResponse response = From 9f485ccb0549f1770f893716e5d024fce845c3da Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 22 Oct 2024 16:42:05 -0400 Subject: [PATCH 028/115] dynamic sizing for practice activity toolbar content --- .../multiple_choice_activity.dart | 2 +- .../no_more_practice_card.dart | 22 ++---- .../practice_activity_card.dart | 76 ++++++++----------- 3 files changed, 41 insertions(+), 59 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 5a1f50497..f0ad5b80a 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -94,7 +94,7 @@ class MultipleChoiceActivityState extends State { Widget build(BuildContext context) { final PracticeActivityModel practiceActivity = widget.currentActivity; - return Container( + return Padding( padding: const EdgeInsets.all(8), child: Column( children: [ diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart index 1cef6c174..d4844ac21 100644 --- a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -71,24 +71,16 @@ class GamifiedTextWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( + return Padding( + padding: const EdgeInsets.all(8), child: Column( - mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children children: [ - const SizedBox(height: 10), // Spacing between the star and text - // Star animation above the text const StarAnimationWidget(), - const SizedBox(height: 10), // Spacing between the star and text - Container( - constraints: const BoxConstraints( - minHeight: 80, - ), - padding: const EdgeInsets.all(8), - child: Text( - userMessage, - style: BotStyle.text(context), - textAlign: TextAlign.center, // Center-align the text - ), + const SizedBox(height: 10), + Text( + userMessage, + style: BotStyle.text(context), + textAlign: TextAlign.center, ), ], ), diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 82d3c0a17..113c7573e 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -306,54 +306,44 @@ class MessagePracticeActivityCardState extends State { } } - String? get userMessage { - if (!fetchingActivity && currentActivity == null) { - return L10n.of(context)!.noActivitiesFound; - } - return null; - } - @override Widget build(BuildContext context) { - if (userMessage != null) { - return GamifiedTextWidget(userMessage: userMessage!); + if (!fetchingActivity && currentActivity == null) { + return GamifiedTextWidget( + userMessage: L10n.of(context)!.noActivitiesFound, + ); } - return Row( - mainAxisSize: MainAxisSize.min, + return Stack( + alignment: Alignment.center, children: [ - Stack( - alignment: Alignment.center, - children: [ - // Main content - const Positioned( - child: PointsGainedAnimation(), - ), - Container( - padding: const EdgeInsets.all(8), - child: activityWidget, - ), - // Conditionally show the darkening and progress indicator based on the loading state - if (!savoringTheJoy && fetchingActivity) ...[ - // Semi-transparent overlay - Container( - color: Colors.black.withOpacity(0.5), // Darkening effect - ), - // Circular progress indicator in the center - const Center( - child: CircularProgressIndicator(), - ), - ], - // Flag button in the top right corner - Positioned( - top: 0, - right: 0, - child: ContentIssueButton( - isActive: currentActivity != null, - submitFeedback: submitFeedback, - ), - ), - ], + // Main content + const Positioned( + child: PointsGainedAnimation(), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 20, 8, 8), + child: activityWidget, + ), + // Conditionally show the darkening and progress indicator based on the loading state + if (!savoringTheJoy && fetchingActivity) ...[ + // Semi-transparent overlay + Container( + color: Colors.black.withOpacity(0.5), // Darkening effect + ), + // Circular progress indicator in the center + const Center( + child: CircularProgressIndicator(), + ), + ], + // Flag button in the top right corner + Positioned( + top: 0, + right: 0, + child: ContentIssueButton( + isActive: currentActivity != null, + submitFeedback: submitFeedback, + ), ), ], ); From d5eee79f4cfd14c5182b2f00e2f645261afaccc7 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 09:08:32 -0400 Subject: [PATCH 029/115] log actual error in message_audio_card logging statements instead of empty exception --- lib/pangea/widgets/chat/message_audio_card.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 82375bacf..7a003a01c 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -89,8 +89,7 @@ class MessageAudioCardState extends State { // should never happen but just in case debugger(when: kDebugMode); ErrorHandler.logError( - e: Exception(), - m: 'audioFile duration is null in MessageAudioCardState', + e: 'audioFile duration is null in MessageAudioCardState', data: { 'audioFile': audioFile, }, @@ -124,8 +123,7 @@ class MessageAudioCardState extends State { // if we didn't find the token, we should pause if debug and log an error debugger(when: kDebugMode); ErrorHandler.logError( - e: Exception(), - m: 'could not find token for selection in MessageAudioCardState', + e: 'could not find token for selection in MessageAudioCardState', data: { 'selection': selection, 'tokens': tokens, @@ -174,7 +172,7 @@ class MessageAudioCardState extends State { ), ); ErrorHandler.logError( - e: Exception(), + e: e, s: s, m: 'something wrong getting audio in MessageAudioCardState', data: { From 0b2c32904a031693270d0804c21ab25d938b7345 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 09:21:02 -0400 Subject: [PATCH 030/115] only call setState in message_selection_overlay if mounted --- lib/pangea/widgets/chat/message_selection_overlay.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index db421dd15..21c875681 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -106,7 +106,8 @@ class MessageOverlayController extends State void setState(VoidCallback fn) { if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle || SchedulerBinding.instance.schedulerPhase == - SchedulerPhase.postFrameCallbacks) { + SchedulerPhase.postFrameCallbacks && + mounted) { // It's safe to call setState immediately super.setState(fn); } else { From bc1dfc1e0e8f017c9c4032afcd4b18112042d35b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 09:21:44 -0400 Subject: [PATCH 031/115] when inviting tachers to analytics room, request all particpants to ensure teacher isn't already a member --- .../pangea_room_extension/room_analytics_extension.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 73371b080..b44c40ece 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room { await analyticsRoom.requestParticipants(); } - final List participants = analyticsRoom.getParticipants(); + final List participants = await analyticsRoom.requestParticipants(); final List uninvitedTeachers = teachersLocal .where((teacher) => !participants.contains(teacher)) .toList(); @@ -110,8 +110,12 @@ extension AnalyticsRoomExtension on Room { (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { ErrorHandler.logError( e: err, - m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + m: "Failed to invite teacher to analytics room", s: s, + data: { + "teacherId": teacher.id, + "analyticsRoomId": analyticsRoom.id, + }, ); }), ), From 7e9855dcc1eae311a65f8f00a36bfcf87376e2bd Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 09:34:03 -0400 Subject: [PATCH 032/115] better error logging if sourceText is null in getNextTranslationData --- .../choreographer/controllers/it_controller.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 636415b8e..b618386f8 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -4,7 +4,6 @@ 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/construct_use_type_enum.dart'; -import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -180,6 +179,18 @@ class ITController { } Future getNextTranslationData() async { + if (sourceText == null) { + ErrorHandler.logError( + e: Exception("sourceText is null in getNextTranslationData"), + data: { + "sourceText": sourceText, + "currentITStep": currentITStep, + "nextITStep": nextITStep, + }, + ); + return; + } + try { if (completedITSteps.length < goldRouteTracker.continuances.length) { final String currentText = choreographer.currentText; From 7c0078694b9567e4ccc4a97a4152a877899bccba Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 10:06:16 -0400 Subject: [PATCH 033/115] adding mounted check --- lib/pages/chat/events/message_content.dart | 2 -- .../widgets/practice_activity/practice_activity_card.dart | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 2cddb6f67..4e21f00ac 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -123,7 +123,6 @@ class MessageContent extends StatelessWidget { @override Widget build(BuildContext context) { - // debugger(when: overlayController != null); final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final buttonTextColor = textColor; switch (event.type) { @@ -307,7 +306,6 @@ class MessageContent extends StatelessWidget { height: 1.3, ); - // debugger(when: overlayController != null); if (overlayController != null && pangeaMessageEvent != null) { return OverlayMessageText( pangeaMessageEvent: pangeaMessageEvent!, diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 113c7573e..fd4428b59 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -177,7 +177,7 @@ class MessagePracticeActivityCardState extends State { Future _savorTheJoy() async { debugger(when: savoringTheJoy && kDebugMode); - setState(() => savoringTheJoy = true); + if (mounted) setState(() => savoringTheJoy = true); await Future.delayed(appropriateTimeForJoy); From 5d190cc51e1d0d96e32e232e93ffa0305e060d33 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 10:10:49 -0400 Subject: [PATCH 034/115] check for null content in message translation card --- .../chat/message_translation_card.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 81ed7a2e2..4d8bad28d 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -144,14 +144,18 @@ class MessageTranslationCardState extends State { child: Column( children: [ widget.selection != null - ? Text( - selectionTranslation!, - style: BotStyle.text(context), - ) - : Text( - repEvent!.text, - style: BotStyle.text(context), - ), + ? selectionTranslation != null + ? Text( + selectionTranslation!, + style: BotStyle.text(context), + ) + : const ToolbarContentLoadingIndicator() + : repEvent != null + ? Text( + repEvent!.text, + style: BotStyle.text(context), + ) + : const ToolbarContentLoadingIndicator(), if (notGoingToTranslate && widget.selection == null) InlineTooltip( instructionsEnum: InstructionsEnum.l1Translation, From 3efe3743023c996049caba48fe8b30c074edebd9 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 10:12:40 -0400 Subject: [PATCH 035/115] added mounted check before setting state in choice_array --- lib/pangea/choreographer/widgets/choice_array.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index ff13da78f..32395099e 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -44,17 +44,13 @@ class ChoicesArrayState extends State { void disableInteraction() { WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - interactionDisabled = true; - }); + if (mounted) setState(() => interactionDisabled = true); }); } void enableInteractions() { WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - interactionDisabled = false; - }); + if (mounted) setState(() => interactionDisabled = false); }); } From 696bd0f1299a8805520f476b61c5f811c9327b98 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 10:57:05 -0400 Subject: [PATCH 036/115] in message overlay, wrap any calls to get renderbox or media query in a try catch block to get better error handling --- .../chat/message_selection_overlay.dart | 121 ++++++++++++------ 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 21c875681..7c91a87bc 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart'; @@ -261,14 +262,14 @@ class MessageOverlayController extends State @override void didChangeDependencies() { super.didChangeDependencies(); - if (messageSize == null || messageOffset == null) { + if (messageSize == null || messageOffset == null || screenHeight == null) { return; } // position the overlay directly over the underlying message - final headerBottomOffset = screenHeight - headerHeight; + final headerBottomOffset = screenHeight! - headerHeight; final footerBottomOffset = footerHeight; - final currentBottomOffset = screenHeight - + final currentBottomOffset = screenHeight! - messageOffset!.dy - messageSize!.height - belowMessageHeight; @@ -296,7 +297,7 @@ class MessageOverlayController extends State animationEndOffset = midpoint - messageSize!.height - belowMessageHeight; final totalTopOffset = animationEndOffset + messageSize!.height + AppConfig.toolbarMaxHeight; - final remainingSpace = screenHeight - totalTopOffset; + final remainingSpace = screenHeight! - totalTopOffset; if (remainingSpace < headerHeight) { // the overlay could run over the header, so it needs to be shifted down animationEndOffset -= (headerHeight - remainingSpace); @@ -311,7 +312,7 @@ class MessageOverlayController extends State // update the message height to fit the screen. The message is scrollable, so // this will make the both the toolbar box and the toolbar buttons visible. if (animationEndOffset < footerHeight + belowMessageHeight) { - final double remainingSpace = screenHeight - + final double remainingSpace = screenHeight! - AppConfig.toolbarMaxHeight - headerHeight - footerHeight - @@ -349,25 +350,57 @@ class MessageOverlayController extends State super.dispose(); } - RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox( + RenderBox? get messageRenderBox { + try { + return MatrixState.pAnyState.getRenderBox( widget._event.eventId, ); + } catch (e, s) { + ErrorHandler.logError(e: "Error getting message render box: $e", s: s); + return null; + } + } + + Size? get messageSize { + try { + return messageRenderBox?.size; + } catch (e, s) { + ErrorHandler.logError(e: "Error getting message size: $e", s: s); + return null; + } + } + + Offset? get messageOffset { + try { + return messageRenderBox?.localToGlobal(Offset.zero); + } catch (e, s) { + ErrorHandler.logError(e: "Error getting message offset: $e", s: s); + return null; + } + } - Size? get messageSize => messageRenderBox?.size; - Offset? get messageOffset => messageRenderBox?.localToGlobal(Offset.zero); double? adjustedMessageHeight; // height of the reply/forward bar + the reaction picker + contextual padding double get footerHeight => 48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0); + MediaQueryData? get mediaQuery { + try { + return MediaQuery.of(context); + } catch (e, s) { + ErrorHandler.logError(e: "Error getting media query: $e", s: s); + return null; + } + } + double get headerHeight => (Theme.of(context).appBarTheme.toolbarHeight ?? 56) + - MediaQuery.of(context).padding.top; + (mediaQuery?.padding.top ?? 0); - double get screenHeight => MediaQuery.of(context).size.height; + double? get screenHeight => mediaQuery?.size.height; - double get screenWidth => MediaQuery.of(context).size.width; + double? get screenWidth => mediaQuery?.size.width; @override Widget build(BuildContext context) { @@ -381,13 +414,17 @@ class MessageOverlayController extends State // the default spacing between the side of the screen and the message bubble const double messageMargin = Avatar.defaultSize + 16 + 8; final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final chatViewWidth = screenWidth - - (FluffyThemes.isColumnMode(context) - ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) - : 0); + const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin; - double maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin; - if (maxWidth > totalMaxWidth) { + double? maxWidth; + if (screenWidth != null) { + final chatViewWidth = screenWidth! - + (FluffyThemes.isColumnMode(context) + ? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth) + : 0); + maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin; + } + if (maxWidth == null || maxWidth > totalMaxWidth) { maxWidth = totalMaxWidth; } @@ -450,34 +487,36 @@ class MessageOverlayController extends State ? null : messageOffset!.dx - horizontalPadding - columnOffset; - final double? rightPadding = widget._pangeaMessageEvent.ownMessage - ? screenWidth - - messageOffset!.dx - - messageSize!.width - - horizontalPadding - : null; - - final positionedOverlayMessage = _overlayPositionAnimation == null - ? Positioned( - left: leftPadding, - right: rightPadding, - bottom: screenHeight - - messageOffset!.dy - - messageSize!.height - - belowMessageHeight, - child: overlayMessage, - ) - : AnimatedBuilder( - animation: _overlayPositionAnimation!, - builder: (context, child) { - return Positioned( + final double? rightPadding = + (widget._pangeaMessageEvent.ownMessage && screenWidth != null) + ? screenWidth! - + messageOffset!.dx - + messageSize!.width - + horizontalPadding + : null; + + final positionedOverlayMessage = + (_overlayPositionAnimation == null || screenHeight == null) + ? Positioned( left: leftPadding, right: rightPadding, - bottom: _overlayPositionAnimation!.value, + bottom: screenHeight! - + messageOffset!.dy - + messageSize!.height - + belowMessageHeight, child: overlayMessage, + ) + : AnimatedBuilder( + animation: _overlayPositionAnimation!, + builder: (context, child) { + return Positioned( + left: leftPadding, + right: rightPadding, + bottom: _overlayPositionAnimation!.value, + child: overlayMessage, + ); + }, ); - }, - ); return Padding( padding: EdgeInsets.only( From f6bab9273340fabaeaaf5766fa6b338ab517c05e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 11:19:30 -0400 Subject: [PATCH 037/115] better error handling for renderbox errors --- .../chat/message_selection_overlay.dart | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 7c91a87bc..48cdfbd47 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -105,17 +105,29 @@ class MessageOverlayController extends State /// This is a workaround to prevent that error @override void setState(VoidCallback fn) { - if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle || - SchedulerBinding.instance.schedulerPhase == - SchedulerPhase.postFrameCallbacks && - mounted) { + final phase = SchedulerBinding.instance.schedulerPhase; + if (mounted && + (phase == SchedulerPhase.idle || + phase == SchedulerPhase.postFrameCallbacks)) { // It's safe to call setState immediately - super.setState(fn); + try { + super.setState(fn); + } catch (e, s) { + ErrorHandler.logError( + e: "Error calling setState in MessageSelectionOverlay: $e", + s: s, + ); + } } else { // Defer the setState call to after the current frame WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - super.setState(fn); + try { + if (mounted) super.setState(fn); + } catch (e, s) { + ErrorHandler.logError( + e: "Error calling setState in MessageSelectionOverlay after postframeCallback: $e", + s: s, + ); } }); } @@ -404,6 +416,8 @@ class MessageOverlayController extends State @override Widget build(BuildContext context) { + if (messageSize == null) return const SizedBox.shrink(); + final bool showDetails = (Matrix.of(context) .store .getBool(SettingKeys.displayChatDetailsColumn) ?? @@ -483,21 +497,25 @@ class MessageOverlayController extends State ? FluffyThemes.columnWidth + FluffyThemes.navRailWidth : 0; - final double? leftPadding = widget._pangeaMessageEvent.ownMessage - ? null - : messageOffset!.dx - horizontalPadding - columnOffset; - - final double? rightPadding = - (widget._pangeaMessageEvent.ownMessage && screenWidth != null) - ? screenWidth! - - messageOffset!.dx - - messageSize!.width - - horizontalPadding - : null; - - final positionedOverlayMessage = - (_overlayPositionAnimation == null || screenHeight == null) - ? Positioned( + final double? leftPadding = + (widget._pangeaMessageEvent.ownMessage || messageOffset == null) + ? null + : messageOffset!.dx - horizontalPadding - columnOffset; + + final double? rightPadding = (widget._pangeaMessageEvent.ownMessage && + screenWidth != null && + messageOffset != null && + messageSize != null) + ? screenWidth! - + messageOffset!.dx - + messageSize!.width - + horizontalPadding + : null; + + final positionedOverlayMessage = (_overlayPositionAnimation == null) + ? (screenHeight == null || messageSize == null || messageOffset == null) + ? const SizedBox.shrink() + : Positioned( left: leftPadding, right: rightPadding, bottom: screenHeight! - @@ -506,17 +524,17 @@ class MessageOverlayController extends State belowMessageHeight, child: overlayMessage, ) - : AnimatedBuilder( - animation: _overlayPositionAnimation!, - builder: (context, child) { - return Positioned( - left: leftPadding, - right: rightPadding, - bottom: _overlayPositionAnimation!.value, - child: overlayMessage, - ); - }, + : AnimatedBuilder( + animation: _overlayPositionAnimation!, + builder: (context, child) { + return Positioned( + left: leftPadding, + right: rightPadding, + bottom: _overlayPositionAnimation!.value, + child: overlayMessage, ); + }, + ); return Padding( padding: EdgeInsets.only( From dfe1ca6653cd21c6b32bcc4a82e439ffb0559549 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 11:48:45 -0400 Subject: [PATCH 038/115] commenting out audio button in debug attempt --- .../widgets/chat/missing_voice_button.dart | 8 ++++--- .../multiple_choice_activity.dart | 17 ++++++------- .../practice_activity_card.dart | 24 +++++++++++++++---- .../practice_activity/word_audio_button.dart | 6 ++++- pubspec.yaml | 2 +- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart index e1f8b74fb..3baa8422e 100644 --- a/lib/pangea/widgets/chat/missing_voice_button.dart +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -49,9 +49,11 @@ class MissingVoiceButton extends StatelessWidget { ), TextButton( onPressed: () => launchTTSSettings, - style: const ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), + // commenting out as suspecting this is causing an issue + // #freeze-activity + // style: const ButtonStyle( + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // ), child: Text(L10n.of(context)!.openVoiceSettings), ), ], diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index f0ad5b80a..8a34dfc6d 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -3,11 +3,9 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; -import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -85,9 +83,11 @@ class MultipleChoiceActivityState extends State { widget.practiceCardController.onActivityFinish(); } - setState( - () => selectedChoiceIndex = index, - ); + if (mounted) { + setState( + () => selectedChoiceIndex = index, + ); + } } @override @@ -106,9 +106,10 @@ class MultipleChoiceActivityState extends State { ), ), const SizedBox(height: 8), - if (practiceActivity.activityType == - ActivityTypeEnum.wordFocusListening) - WordAudioButton(text: practiceActivity.content.answer), + // #freeze-activity + // if (practiceActivity.activityType == + // ActivityTypeEnum.wordFocusListening) + // WordAudioButton(text: practiceActivity.content.answer), ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index fd4428b59..62dadc78b 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -175,13 +175,26 @@ class MessagePracticeActivityCardState extends State { ); Future _savorTheJoy() async { - debugger(when: savoringTheJoy && kDebugMode); + try { + debugger(when: savoringTheJoy && kDebugMode); - if (mounted) setState(() => savoringTheJoy = true); + if (mounted) setState(() => savoringTheJoy = true); - await Future.delayed(appropriateTimeForJoy); + await Future.delayed(appropriateTimeForJoy); - if (mounted) setState(() => savoringTheJoy = false); + if (mounted) setState(() => savoringTheJoy = false); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: e, + s: s, + m: 'Failed to savor the joy', + data: { + 'activity': currentActivity, + 'record': currentCompletionRecord, + }, + ); + } } /// Called when the user finishes an activity. @@ -211,7 +224,8 @@ class MessagePracticeActivityCardState extends State { widget.pangeaMessageEvent.eventId, ); - // + // wait for the joy to be savored before resolving the activity + // and setting it to replace the previous activity final Iterable result = await Future.wait([ _savorTheJoy(), _fetchNewActivity(), diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 226328804..78c0efb7d 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -22,6 +22,7 @@ class WordAudioButtonState extends State { @override void initState() { // TODO: implement initState + debugPrint('initState WordAudioButton'); super.initState(); ttsController.setupTTS().then((value) => setState(() {})); } @@ -34,6 +35,7 @@ class WordAudioButtonState extends State { @override Widget build(BuildContext context) { + debugPrint('build WordAudioButton'); return Column( children: [ IconButton( @@ -67,7 +69,9 @@ class WordAudioButtonState extends State { } }, // Disable button if language isn't supported ), - ttsController.missingVoiceButton, + // #freeze-activity + //commenting out to see if it's causing an issue + // ttsController.missingVoiceButton, ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 1af58cbb5..2d2c27543 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.5+3541 +version: 1.21.5+3542 environment: sdk: ">=3.0.0 <4.0.0" From a7a7f4c252c1257fbf4ae1aae4d52b2fa963b3fe Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 12:40:38 -0400 Subject: [PATCH 039/115] switch form dropdowns to new wigget type to make options appear below the dropdown --- assets/l10n/intl_en.arb | 7 ++++--- .../conversation_bot_mode_dynamic_zone.dart | 4 ++++ .../conversation_bot_mode_select.dart | 20 +++---------------- .../conversation_bot_settings_form.dart | 12 +++++------ .../space/language_level_dropdown.dart | 8 ++------ pubspec.lock | 18 ++++++++--------- pubspec.yaml | 1 + 7 files changed, 29 insertions(+), 41 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 51ab9d1d7..d7eb4b149 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4016,9 +4016,9 @@ "conversationBotModeSelectOption_storyGame": "Story Game", "conversationBotDiscussionZone_title": "Discussion Settings", "conversationBotDiscussionZone_discussionTopicLabel": "Discussion Topic", - "conversationBotDiscussionZone_discussionTopicPlaceholder": "Set Discussion Topic", + "conversationBotDiscussionZone_discussionTopicPlaceholder": "Set discussion topic", "conversationBotDiscussionZone_discussionKeywordsLabel": "Discussion Keywords", - "conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set Discussion Keywords", + "conversationBotDiscussionZone_discussionKeywordsPlaceholder": "Set discussion keywords", "conversationBotDiscussionZone_discussionKeywordsHintText": "Comma separated list of keywords to guide the discussion", "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel": "Send discussion prompt on a schedule", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts", @@ -4364,5 +4364,6 @@ "selectBotLanguage": "Select bot language", "chooseVoice": "Choose a voice", "enterLanguageLevel": "Please enter a language level", - "enterDiscussionTopic": "Please enter a discussion topic" + "enterDiscussionTopic": "Please enter a discussion topic", + "selectBotChatMode": "Select chat mode" } \ No newline at end of file diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index e8f33fe3d..5a3082610 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -27,6 +27,8 @@ class ConversationBotModeDynamicZone extends StatelessWidget { decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotDiscussionZone_discussionTopicPlaceholder, + contentPadding: + const EdgeInsets.symmetric(horizontal: 28.0, vertical: 12.0), ), controller: discussionTopicController, validator: (value) => enabled && @@ -44,6 +46,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotDiscussionZone_discussionKeywordsPlaceholder, + contentPadding: const EdgeInsets.symmetric(horizontal: 28.0), ), controller: discussionKeywordsController, enabled: enabled, @@ -58,6 +61,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotCustomZone_customSystemPromptPlaceholder, + contentPadding: const EdgeInsets.symmetric(horizontal: 28.0), ), validator: (value) => enabled && botOptions.mode == BotMode.custom && diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index 3d893f078..408e6560e 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -26,23 +27,8 @@ class ConversationBotModeSelect extends StatelessWidget { // L10n.of(context)!.conversationBotModeSelectOption_storyGame, }; - String? mode = initialMode; - if (!options.containsKey(initialMode)) { - mode = null; - } - - return DropdownButtonFormField( - // Initial Value - hint: Text( - options[mode ?? BotMode.discussion]!, - overflow: TextOverflow.clip, - textAlign: TextAlign.center, - ), - // ), - isExpanded: true, - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items + return DropdownButtonFormField2( + hint: Text(L10n.of(context)!.selectBotChatMode), items: [ for (final entry in options.entries) DropdownMenuItem( diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart index 195d35801..6b5535bc4 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart'; @@ -36,8 +37,10 @@ class ConversationBotSettingsForm extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - DropdownButtonFormField( - // Initial Value + DropdownButtonFormField2( + dropdownStyleData: const DropdownStyleData( + padding: EdgeInsets.zero, + ), hint: Text( L10n.of(context)!.selectBotLanguage, overflow: TextOverflow.clip, @@ -45,7 +48,6 @@ class ConversationBotSettingsForm extends StatelessWidget { ), value: botOptions.targetLanguage, isExpanded: true, - icon: const Icon(Icons.keyboard_arrow_down), items: MatrixState.pangeaController.pLanguageStore.targetOptions .map((language) { return DropdownMenuItem( @@ -60,8 +62,7 @@ class ConversationBotSettingsForm extends StatelessWidget { onChanged: enabled ? onUpdateBotLanguage : null, ), const SizedBox(height: 12), - DropdownButtonFormField( - // Initial Value + DropdownButtonFormField2( hint: Text( L10n.of(context)!.chooseVoice, overflow: TextOverflow.clip, @@ -69,7 +70,6 @@ class ConversationBotSettingsForm extends StatelessWidget { ), value: botOptions.targetVoice, isExpanded: true, - icon: const Icon(Icons.keyboard_arrow_down), items: const [], onChanged: enabled ? onUpdateBotVoice : null, ), diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index 0b238485a..a8c618bc1 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/utils/language_level_copy.dart'; import 'package:flutter/material.dart'; @@ -19,18 +20,13 @@ class LanguageLevelDropdown extends StatelessWidget { @override Widget build(BuildContext context) { - return DropdownButtonFormField( - // Initial Value + return DropdownButtonFormField2( hint: Text( L10n.of(context)!.selectLanguageLevel, overflow: TextOverflow.clip, textAlign: TextAlign.center, ), value: initialLevel, - isExpanded: true, - // Down Arrow Icon - icon: const Icon(Icons.keyboard_arrow_down), - // Array list of items items: LanguageLevelType.allInts.map((int levelOption) { return DropdownMenuItem( value: levelOption, diff --git a/pubspec.lock b/pubspec.lock index 0a0e1ed23..99543db43 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -345,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 + url: "https://pub.dev" + source: hosted + version: "2.3.9" dynamic_color: dependency: "direct main" description: @@ -2642,14 +2650,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" - visibility_detector: - dependency: transitive - description: - name: visibility_detector - sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" - url: "https://pub.dev" - source: hosted - version: "0.3.3" vm_service: dependency: transitive description: @@ -2723,7 +2723,7 @@ packages: source: hosted version: "1.2.0" win32: - dependency: "direct overridden" + dependency: transitive description: name: win32 sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" diff --git a/pubspec.yaml b/pubspec.yaml index 1af58cbb5..1dc0fcc8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -113,6 +113,7 @@ dependencies: android_intent_plus: ^5.2.0 country_picker: ^2.0.25 csv: ^6.0.0 + dropdown_button2: ^2.3.9 fl_chart: ^0.67.0 firebase_analytics: ^11.0.1 firebase_core: ^3.1.0 From 058f876146a4792d9eb7a49806b8a94515beabf3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 12:46:16 -0400 Subject: [PATCH 040/115] copyeditting and updated order of main menu options --- assets/l10n/intl_en.arb | 2 +- .../chat_list/client_chooser_button.dart | 51 ++++++++++--------- lib/pages/new_group/new_group_view.dart | 5 +- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 51ab9d1d7..87e134d03 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3095,7 +3095,7 @@ "type": "text", "placeholders": {} }, - "learningSettings": "My Learning Settings", + "learningSettings": "Learning settings", "classNameRequired": "Please enter a space name", "@classNameRequired": { "type": "text", diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index b22512bbe..6073034bd 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -41,44 +41,41 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - PopupMenuItem( - value: SettingsAction.learning, - child: Row( - children: [ - const Icon(Icons.psychology_outlined), - const SizedBox(width: 18), - Expanded(child: Text(L10n.of(context)!.learningSettings)), - ], - ), - ), + // PopupMenuItem( + // value: SettingsAction.newGroup, + // child: Row( + // children: [ + // const Icon(Icons.group_add_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.createGroup), + // ], + // ), + // ), // Pangea# PopupMenuItem( - value: SettingsAction.newGroup, + value: SettingsAction.newSpace, child: Row( children: [ - const Icon(Icons.group_add_outlined), + const Icon(Icons.workspaces_outlined), const SizedBox(width: 18), // #Pangea - Expanded(child: Text(L10n.of(context)!.createGroup)), - // Text(L10n.of(context)!.createGroup), + Text(L10n.of(context)!.createNewSpace), + // Text(L10n.of(context)!.createNewSpace), // Pangea# ], ), ), + // #Pangea PopupMenuItem( - value: SettingsAction.newSpace, + value: SettingsAction.learning, child: Row( children: [ - const Icon(Icons.workspaces_outlined), + const Icon(Icons.psychology_outlined), const SizedBox(width: 18), - // #Pangea - Text(L10n.of(context)!.createNewSpace), - // Text(L10n.of(context)!.createNewSpace), - // Pangea# + Expanded(child: Text(L10n.of(context)!.learningSettings)), ], ), ), - // #Pangea // PopupMenuItem( // value: SettingsAction.setStatus, // child: Row( @@ -306,9 +303,11 @@ class ClientChooserButton extends StatelessWidget { if (consent != OkCancelResult.ok) return; context.go('/rooms/settings/addaccount'); break; - case SettingsAction.newGroup: - context.go('/rooms/newgroup'); - break; + // #Pangea + // case SettingsAction.newGroup: + // context.go('/rooms/newgroup'); + // break; + // Pangea# case SettingsAction.newSpace: controller.createNewSpace(); break; @@ -416,7 +415,9 @@ class ClientChooserButton extends StatelessWidget { enum SettingsAction { addAccount, - newGroup, + // #Pangea + // newGroup, + // Pangea# newSpace, // #Pangea // setStatus, diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index addf7b5f7..7ea7f9de5 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -24,7 +24,10 @@ class NewGroupView extends StatelessWidget { onPressed: controller.loading ? null : Navigator.of(context).pop, ), ), - title: Text(L10n.of(context)!.createGroup), + // #Pangea + // title: Text(L10n.of(context)!.createGroup), + title: Text(L10n.of(context)!.newChat), + // Pangea# ), body: MaxWidthBody( child: Column( From d570b772bc4bfae775a91f211f2347b68cc987f3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 12:47:46 -0400 Subject: [PATCH 041/115] switch from 'my learning settings' -> 'learning settings' --- assets/l10n/intl_en.arb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 87e134d03..5c994c841 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2563,7 +2563,7 @@ "type": "text", "placeholders": {} }, - "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > My Learning Settings.", + "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > Learning Settings.", "@interactiveTranslatorAllowedDesc": { "type": "text", "placeholders": {} @@ -3030,9 +3030,9 @@ "errorDisableLanguageAssistanceClassDesc": "Translation assistance and grammar assistance are turned off for the space that this chat is in.", "itIsDisabled": "Interactive Translation is disabled", "igcIsDisabled": "Interactive Grammar Checking is disabled", - "goToLearningSettings": "Go to My Learning Settings", + "goToLearningSettings": "Go to Learning Settings", "error405Title": "Languages not set", - "error405Desc": "Please set your languages in Main Menu > My Learning Settings.", + "error405Desc": "Please set your languages in Main Menu > Learning Settings.", "loginOrSignup": "Sign in with", "@loginOrSignup": { "type": "text", From 7b2defab02c71437333ea96e1b72612bfa95d0cf Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 14:00:16 -0400 Subject: [PATCH 042/115] adjust oerlay offset on overflow --- lib/pages/chat_list/space_view.dart | 4 +-- lib/pangea/utils/overlay.dart | 52 ++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index a45d28f43..f0f2eda05 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -317,14 +317,14 @@ class _SpaceViewState extends State { key: AddRoomType.subspace, // #Pangea // label: L10n.of(context)!.createNewSpace, - label: L10n.of(context)!.newChat, + label: L10n.of(context)!.newSpace, // Pangea# ), AlertDialogAction( key: AddRoomType.chat, // #Pangea // label: L10n.of(context)!.createGroup, - label: L10n.of(context)!.createChat, + label: L10n.of(context)!.newChat, // Pangea# ), ], diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 5bf8a5823..ef7a16b80 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -20,17 +20,14 @@ class OverlayUtil { required BuildContext context, required Widget child, required String transformTargetId, - double? width, - double? height, backDropToDismiss = true, blurBackground = false, Color? borderColor, Color? backgroundColor, - Alignment? targetAnchor, - Alignment? followerAnchor, bool closePrevOverlay = true, Function? onDismiss, OverlayPositionEnum position = OverlayPositionEnum.transform, + Offset? offset, }) { try { if (closePrevOverlay) { @@ -54,18 +51,16 @@ class OverlayUtil { right: (position == OverlayPositionEnum.centered) ? 0 : null, left: (position == OverlayPositionEnum.centered) ? 0 : null, bottom: (position == OverlayPositionEnum.centered) ? 0 : null, - width: width, - height: height, child: (position != OverlayPositionEnum.transform) ? child : CompositedTransformFollower( - targetAnchor: targetAnchor ?? Alignment.topCenter, - followerAnchor: - followerAnchor ?? Alignment.bottomCenter, + targetAnchor: Alignment.topCenter, + followerAnchor: Alignment.bottomCenter, link: MatrixState.pAnyState .layerLinkAndKey(transformTargetId) .link, showWhenUnlinked: false, + offset: offset ?? Offset.zero, child: child, ), ), @@ -100,6 +95,32 @@ class OverlayUtil { return; } + Offset offset = Offset.zero; + final RenderBox? targetRenderBox = + layerLinkAndKey.key.currentContext!.findRenderObject() as RenderBox?; + if (targetRenderBox != null && targetRenderBox.hasSize) { + final Offset transformTargetOffset = + (targetRenderBox).localToGlobal(Offset.zero); + final Size transformTargetSize = targetRenderBox.size; + final horizontalMidpoint = + transformTargetOffset.dx + (transformTargetSize.width / 2); + + final halfMaxWidth = maxWidth / 2; + final hasLeftOverflow = (horizontalMidpoint - halfMaxWidth) < 0; + final hasRightOverflow = (horizontalMidpoint + halfMaxWidth) > + MediaQuery.of(context).size.width; + double xOffset = 0; + + MediaQuery.of(context).size.width - (horizontalMidpoint + halfMaxWidth); + if (hasLeftOverflow) { + xOffset = (transformTargetOffset.dx - halfMaxWidth) * -1; + } else if (hasRightOverflow) { + xOffset = MediaQuery.of(context).size.width - + (horizontalMidpoint + halfMaxWidth); + } + offset = Offset(xOffset, 0); + } + final Widget child = Material( borderOnForeground: false, color: Colors.transparent, @@ -119,6 +140,7 @@ class OverlayUtil { backDropToDismiss: backDropToDismiss, borderColor: borderColor, closePrevOverlay: closePrevOverlay, + offset: offset, ); } catch (err, stack) { debugger(when: kDebugMode); @@ -138,12 +160,12 @@ class OverlayUtil { // final OverlayConstraints constraints = // ChatViewConstraints(transformTargetContext); - // final RenderObject? targetRenderBox = - // transformTargetContext.findRenderObject(); - // if (targetRenderBox == null) return Offset.zero; - // final Offset transformTargetOffset = - // (targetRenderBox as RenderBox).localToGlobal(Offset.zero); - // final Size transformTargetSize = targetRenderBox.size; + // final RenderObject? targetRenderBox = + // transformTargetContext.findRenderObject(); + // if (targetRenderBox == null) return Offset.zero; + // final Offset transformTargetOffset = + // (targetRenderBox as RenderBox).localToGlobal(Offset.zero); + // final Size transformTargetSize = targetRenderBox.size; // // ideally horizontally centered on target // double dx = transformTargetSize.width / 2 - cardSize.width / 2; From 0fa4202f7dffada5f84a7fd9431f96505c403e5e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 14:23:11 -0400 Subject: [PATCH 043/115] always show learning settings in popup --- lib/config/routes.dart | 10 ------ .../chat_list/client_chooser_button.dart | 6 +++- lib/pages/settings/settings_view.dart | 5 --- .../language_permissions_warning_buttons.dart | 9 +++-- .../settings_learning/settings_learning.dart | 6 +--- .../settings_learning_view.dart | 33 +++++++++++++++---- .../find_conversation_partner_dialog.dart | 8 ----- lib/widgets/chat_settings_popup_menu.dart | 23 +------------ 8 files changed, 40 insertions(+), 60 deletions(-) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 515b25fd9..86a0ca75e 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -29,7 +29,6 @@ import 'package:fluffychat/pages/settings_style/settings_style.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart'; import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart'; -import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; import 'package:fluffychat/pangea/pages/sign_up/signup.dart'; import 'package:fluffychat/pangea/widgets/class/join_with_link.dart'; @@ -406,15 +405,6 @@ abstract class AppRoutes { ], ), // #Pangea - GoRoute( - path: 'learning', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SettingsLearning(), - ), - redirect: loggedOutRedirect, - ), GoRoute( path: 'subscription', pageBuilder: (context, state) => defaultPageBuilder( diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 6073034bd..34fda564b 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,4 +1,5 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/utils/logout.dart'; import 'package:fluffychat/pangea/utils/space_code.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -327,7 +328,10 @@ class ClientChooserButton extends StatelessWidget { // controller.setStatus(); // break; case SettingsAction.learning: - context.go('/rooms/settings/learning'); + showDialog( + context: context, + builder: (c) => const SettingsLearning(), + ); break; case SettingsAction.joinWithClassCode: SpaceCodeUtil.joinWithSpaceCodeDialog( diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index d63776272..cf89cd326 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -162,11 +162,6 @@ class SettingsView extends StatelessWidget { title: Text(L10n.of(context)!.subscriptionManagement), onTap: () => context.go('/rooms/settings/subscription'), ), - ListTile( - leading: const Icon(Icons.psychology_outlined), - title: Text(L10n.of(context)!.learningSettings), - onTap: () => context.go('/rooms/settings/learning'), - ), // Pangea# ListTile( leading: const Icon(Icons.shield_outlined), diff --git a/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart b/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart index fcf3b57be..f595e1316 100644 --- a/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart +++ b/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart @@ -3,12 +3,12 @@ import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import '../../../widgets/matrix.dart'; @@ -52,7 +52,12 @@ class LanguagePermissionsButtons extends StatelessWidget { text: copy.description, style: const TextStyle(color: AppConfig.primaryColor), recognizer: TapGestureRecognizer() - ..onTap = () => context.go('/rooms/settings/learning'), + ..onTap = () { + showDialog( + context: context, + builder: (c) => const SettingsLearning(), + ); + }, ), ], ), diff --git a/lib/pangea/pages/settings_learning/settings_learning.dart b/lib/pangea/pages/settings_learning/settings_learning.dart index 368d0a7c3..3e0a11e4c 100644 --- a/lib/pangea/pages/settings_learning/settings_learning.dart +++ b/lib/pangea/pages/settings_learning/settings_learning.dart @@ -8,11 +8,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; class SettingsLearning extends StatefulWidget { - final bool isPopup; - const SettingsLearning({ - this.isPopup = false, - super.key, - }); + const SettingsLearning({super.key}); @override SettingsLearningController createState() => SettingsLearningController(); diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart index 835dbb774..943d81ca2 100644 --- a/lib/pangea/pages/settings_learning/settings_learning_view.dart +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -13,18 +14,16 @@ class SettingsLearningView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + final dialogContent = Scaffold( appBar: AppBar( centerTitle: true, title: Text( L10n.of(context)!.learningSettings, ), - leading: controller.widget.isPopup - ? IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - ) - : null, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, + ), ), body: ListTileTheme( iconColor: Theme.of(context).textTheme.bodyLarge!.color, @@ -79,5 +78,25 @@ class SettingsLearningView extends StatelessWidget { ), ), ); + + return kIsWeb + ? Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 600, + maxHeight: 600, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: dialogContent, + ), + ), + ) + : Dialog.fullscreen( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: dialogContent, + ), + ); } } diff --git a/lib/pangea/utils/find_conversation_partner_dialog.dart b/lib/pangea/utils/find_conversation_partner_dialog.dart index 035f091d8..149e20c74 100644 --- a/lib/pangea/utils/find_conversation_partner_dialog.dart +++ b/lib/pangea/utils/find_conversation_partner_dialog.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; @@ -27,13 +26,6 @@ void findConversationPartnerDialog( onPressed: Navigator.of(context).pop, child: Text(L10n.of(context)!.cancel), ), - TextButton( - onPressed: () { - context.go('/rooms/settings/learning'); - Navigator.of(context).pop(); - }, - child: Text(L10n.of(context)!.accountSettings), - ), ], ), ); diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index b4271213f..6e216a38a 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/utils/download_chat.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'; @@ -154,27 +153,7 @@ class ChatSettingsPopupMenuState extends State { case ChatPopupMenuActions.learningSettings: showDialog( context: context, - builder: (c) { - return kIsWeb - ? Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 600, - maxHeight: 600, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: const SettingsLearning(isPopup: true), - ), - ), - ) - : Dialog.fullscreen( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: const SettingsLearning(isPopup: true), - ), - ); - }, + builder: (c) => const SettingsLearning(), ); break; // Pangea# From e30267bf8e72bfde8976680414557702905f4102 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 14:34:42 -0400 Subject: [PATCH 044/115] turn off analytics setting in multiple choice --- .../practice_activity_event.dart | 9 ++---- .../widgets/chat/missing_voice_button.dart | 6 ++-- .../multiple_choice_activity.dart | 29 +++++++++---------- .../practice_activity_card.dart | 5 ++-- .../word_focus_listening_activity.dart | 2 +- 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index 5ab1cce31..3de2e2ffc 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -36,13 +36,8 @@ class PracticeActivityEvent { } PracticeActivityModel get practiceActivity { - try { - _content ??= event.getPangeaContent(); - return _content!; - } catch (e, s) { - final contentMap = event.content; - rethrow; - } + _content ??= event.getPangeaContent(); + return _content!; } /// All completion records assosiated with this activity diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart index 3baa8422e..b1f12c626 100644 --- a/lib/pangea/widgets/chat/missing_voice_button.dart +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -51,9 +51,9 @@ class MissingVoiceButton extends StatelessWidget { onPressed: () => launchTTSSettings, // commenting out as suspecting this is causing an issue // #freeze-activity - // style: const ButtonStyle( - // tapTargetSize: MaterialTapTargetSize.shrinkWrap, - // ), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), child: Text(L10n.of(context)!.openVoiceSettings), ), ], diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 8a34dfc6d..16b8e44a6 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,17 +2,15 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// The multiple choice activity view class MultipleChoiceActivity extends StatefulWidget { - final MessagePracticeActivityCardState practiceCardController; + final PracticeActivityCardState practiceCardController; final PracticeActivityModel currentActivity; const MultipleChoiceActivity({ @@ -65,18 +63,19 @@ class MultipleChoiceActivityState extends State { return; } - MatrixState.pangeaController.myAnalytics.setState( - AnalyticsStream( - // note - this maybe should be the activity event id - eventId: - widget.practiceCardController.widget.pangeaMessageEvent.eventId, - roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, - constructs: currentRecordModel!.latestResponse!.toUses( - widget.practiceCardController.currentActivity!, - widget.practiceCardController.metadata, - ), - ), - ); + // #freeze-activity + // MatrixState.pangeaController.myAnalytics.setState( + // AnalyticsStream( + // // note - this maybe should be the activity event id + // eventId: + // widget.practiceCardController.widget.pangeaMessageEvent.eventId, + // roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + // constructs: currentRecordModel!.latestResponse!.toUses( + // widget.practiceCardController.currentActivity!, + // widget.practiceCardController.metadata, + // ), + // ), + // ); // If the selected choice is correct, send the record and get the next activity if (widget.currentActivity.content.isCorrect(value, index)) { diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 62dadc78b..7a2af7fc9 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -36,11 +36,10 @@ class PracticeActivityCard extends StatefulWidget { }); @override - MessagePracticeActivityCardState createState() => - MessagePracticeActivityCardState(); + PracticeActivityCardState createState() => PracticeActivityCardState(); } -class MessagePracticeActivityCardState extends State { +class PracticeActivityCardState extends State { PracticeActivityModel? currentActivity; PracticeActivityRecordModel? currentCompletionRecord; bool fetchingActivity = false; 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 720f784ba..8e22aced8 100644 --- a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; class WordFocusListeningActivity extends StatefulWidget { final PracticeActivityModel activity; - final MessagePracticeActivityCardState practiceCardController; + final PracticeActivityCardState practiceCardController; const WordFocusListeningActivity({ super.key, From 4bfc70300ae1e1893e90fc810e2f9c407424e474 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 14:39:03 -0400 Subject: [PATCH 045/115] bumping version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5b2f1b287..d4327d77b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.5+3542 +version: 1.21.5+3543 environment: sdk: ">=3.0.0 <4.0.0" From 6265d6636d3d50071b1062ae470222b1b49ef882 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 15:13:08 -0400 Subject: [PATCH 046/115] bringing word audio card --- .../practice_activity/multiple_choice_activity.dart | 8 +++++--- .../widgets/practice_activity/word_audio_button.dart | 3 +-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 16b8e44a6..3e8a9f126 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,9 +2,11 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -106,9 +108,9 @@ class MultipleChoiceActivityState extends State { ), const SizedBox(height: 8), // #freeze-activity - // if (practiceActivity.activityType == - // ActivityTypeEnum.wordFocusListening) - // WordAudioButton(text: practiceActivity.content.answer), + if (practiceActivity.activityType == + ActivityTypeEnum.wordFocusListening) + WordAudioButton(text: practiceActivity.content.answer), ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 78c0efb7d..24835fcb2 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -70,8 +70,7 @@ class WordAudioButtonState extends State { }, // Disable button if language isn't supported ), // #freeze-activity - //commenting out to see if it's causing an issue - // ttsController.missingVoiceButton, + ttsController.missingVoiceButton, ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index d4327d77b..7708fc277 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.5+3543 +version: 1.21.5+3544 environment: sdk: ">=3.0.0 <4.0.0" From 6d7ef49d91dba637a8408bb9af0e5d991dfa1b74 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 15:35:50 -0400 Subject: [PATCH 047/115] bring back set analytics in updateChoice --- .../practice_activity_model.dart | 8 +++--- .../multiple_choice_activity.dart | 26 ++++++++++--------- pubspec.yaml | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 644031e47..65f02c4f7 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -187,10 +187,10 @@ class PracticeActivityModel { // moving from multiple_choice to content as the key // this is to make the model more generic // here for backward compatibility - final Map? content = + final Map? contentMap = (json['content'] ?? json["multiple_choice"]) as Map?; - if (content == null) { + if (contentMap == null) { Sentry.addBreadcrumb( Breadcrumb(data: {"json": json}), ); @@ -211,9 +211,7 @@ class PracticeActivityModel { e.string == json['activity_type'] as String || e.string.split('.').last == json['activity_type'] as String, ), - content: ActivityContent.fromJson( - content, - ), + content: ActivityContent.fromJson(contentMap), ); } diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 3e8a9f126..ee892837b 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,11 +2,13 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -66,18 +68,18 @@ class MultipleChoiceActivityState extends State { } // #freeze-activity - // MatrixState.pangeaController.myAnalytics.setState( - // AnalyticsStream( - // // note - this maybe should be the activity event id - // eventId: - // widget.practiceCardController.widget.pangeaMessageEvent.eventId, - // roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, - // constructs: currentRecordModel!.latestResponse!.toUses( - // widget.practiceCardController.currentActivity!, - // widget.practiceCardController.metadata, - // ), - // ), - // ); + MatrixState.pangeaController.myAnalytics.setState( + AnalyticsStream( + // note - this maybe should be the activity event id + eventId: + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + constructs: currentRecordModel!.latestResponse!.toUses( + widget.practiceCardController.currentActivity!, + widget.practiceCardController.metadata, + ), + ), + ); // If the selected choice is correct, send the record and get the next activity if (widget.currentActivity.content.isCorrect(value, index)) { diff --git a/pubspec.yaml b/pubspec.yaml index 7708fc277..43430c50c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.5+3544 +version: 1.21.5+3545 environment: sdk: ">=3.0.0 <4.0.0" From 31b77c6d99093620b5e294d97acb8b5452d5154b Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 23 Oct 2024 16:36:24 -0400 Subject: [PATCH 048/115] some name cleanup, error handling and dont open overlay if click outside text --- .../controllers/choreographer.dart | 10 +++---- .../widgets/start_igc_button.dart | 9 ++++-- .../controllers/subscription_controller.dart | 13 +++++---- .../widgets/igc/pangea_text_controller.dart | 21 ++++++++++---- lib/utils/error_reporter.dart | 28 +++++++++++-------- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index cc016b1ca..df254d49c 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -70,8 +70,8 @@ class Choreographer { void send(BuildContext context) { if (isFetching) return; - if (pangeaController.subscriptionController.canSendStatus == - CanSendStatus.showPaywall) { + if (pangeaController.subscriptionController.subscriptionStatus == + SubscriptionStatus.showPaywall) { OverlayUtil.showPositionedCard( context: context, cardToShow: PaywallCard( @@ -245,10 +245,10 @@ class Choreographer { }) async { try { if (errorService.isError) return; - final CanSendStatus canSendStatus = - pangeaController.subscriptionController.canSendStatus; + final SubscriptionStatus canSendStatus = + pangeaController.subscriptionController.subscriptionStatus; - if (canSendStatus != CanSendStatus.subscribed || + if (canSendStatus != SubscriptionStatus.subscribed || (!igcEnabled && !itEnabled) || (!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) { return; diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 39d35a5a9..f9782e763 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -63,10 +63,13 @@ class StartIGCButtonState extends State bool get itEnabled => widget.controller.choreographer.itEnabled; bool get igcEnabled => widget.controller.choreographer.igcEnabled; - CanSendStatus get canSendStatus => - widget.controller.pangeaController.subscriptionController.canSendStatus; + + SubscriptionStatus get subscriptionStatus => widget + .controller.pangeaController.subscriptionController.subscriptionStatus; + bool get grammarCorrectionEnabled => - (itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed; + (itEnabled || igcEnabled) && + subscriptionStatus == SubscriptionStatus.subscribed; @override Widget build(BuildContext context) { diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart index 73cf77dff..94c396cab 100644 --- a/lib/pangea/controllers/subscription_controller.dart +++ b/lib/pangea/controllers/subscription_controller.dart @@ -23,7 +23,7 @@ import 'package:http/http.dart'; import 'package:purchases_flutter/purchases_flutter.dart'; import 'package:url_launcher/url_launcher_string.dart'; -enum CanSendStatus { +enum SubscriptionStatus { subscribed, dimissedPaywall, showPaywall, @@ -227,11 +227,13 @@ class SubscriptionController extends BaseController { setState(null); } - CanSendStatus get canSendStatus => isSubscribed - ? CanSendStatus.subscribed + /// if the user is subscribed, returns subscribed + /// if the user has dismissed the paywall, returns dismissed + SubscriptionStatus get subscriptionStatus => isSubscribed + ? SubscriptionStatus.subscribed : _shouldShowPaywall - ? CanSendStatus.showPaywall - : CanSendStatus.dimissedPaywall; + ? SubscriptionStatus.showPaywall + : SubscriptionStatus.dimissedPaywall; DateTime? get _lastDismissedPaywall { final lastDismissed = _pangeaController.pStoreService.read( @@ -249,6 +251,7 @@ class SubscriptionController extends BaseController { return backoff; } + /// whether or not the paywall should be shown bool get _shouldShowPaywall { return initialized.isCompleted && !isSubscribed && diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index 378c33ad7..b7bbc1af8 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -47,9 +47,11 @@ class PangeaTextController extends TextEditingController { debugger(when: kDebugMode); return; } - final CanSendStatus canSendStatus = - choreographer.pangeaController.subscriptionController.canSendStatus; - if (canSendStatus == CanSendStatus.showPaywall && + + // show the paywall if appropriate + if (choreographer + .pangeaController.subscriptionController.subscriptionStatus == + SubscriptionStatus.showPaywall && !choreographer.isFetching && text.isNotEmpty) { OverlayUtil.showPositionedCard( @@ -63,11 +65,18 @@ class PangeaTextController extends TextEditingController { ); } + // if there is no igc text data, then don't do anything if (choreographer.igc.igcTextData == null) return; // debugPrint( // "onInputTap matches are ${choreographer.igc.igcTextData?.matches.map((e) => e.match.rule.id).toList().toString()}"); + // if user is just trying to get their cursor into the text input field to add soemthing, + // then don't interrupt them + if (selection.baseOffset >= text.length) { + return; + } + final int tokenIndex = choreographer.igc.igcTextData!.tokenIndexByOffset( selection.baseOffset, ); @@ -147,9 +156,9 @@ class PangeaTextController extends TextEditingController { // debugPrint("composing after ${value.composing.textAfter(value.text)}"); // } - final CanSendStatus canSendStatus = - choreographer.pangeaController.subscriptionController.canSendStatus; - if (canSendStatus == CanSendStatus.showPaywall && + final SubscriptionStatus canSendStatus = choreographer + .pangeaController.subscriptionController.subscriptionStatus; + if (canSendStatus == SubscriptionStatus.showPaywall && !choreographer.isFetching && text.isNotEmpty) { return TextSpan( diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index 650277bf1..2e7d41122 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -11,18 +12,17 @@ class ErrorReporter { void onErrorCallback(Object error, [StackTrace? stackTrace]) async { Logs().e(message ?? 'Error caught', error, stackTrace); // #Pangea - // Attempt to retrieve the L10n instance using the current context - final L10n? l10n = L10n.of(context); - - // Check if the L10n instance is null - if (l10n == null) { - // Log an error message saying that the localization object is null - Logs().e('Localization object is null, cannot show error message.'); - // Exits early to prevent further execution - return; - } - try { + // Attempt to retrieve the L10n instance using the current context + final L10n? l10n = L10n.of(context); + + // Check if the L10n instance is null + if (l10n == null) { + // Log an error message saying that the localization object is null + Logs().e('Localization object is null, cannot show error message.'); + // Exits early to prevent further execution + return; + } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -32,6 +32,12 @@ class ErrorReporter { ); } catch (err) { debugPrint("Failed to show error snackbar."); + } finally { + ErrorHandler.logError( + e: error, + s: stackTrace, + m: message ?? 'Error caught', + ); } } // final text = '$error\n${stackTrace ?? ''}'; From 0c7042b51cee9c9c1d46ef9e92a3463685396acf Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 23 Oct 2024 16:36:40 -0400 Subject: [PATCH 049/115] store num completed activities in memory instead of local cache --- .../practice_activity_record_controller.dart | 105 ++++++++---------- .../pangea_message_event.dart | 3 +- pubspec.yaml | 2 +- 3 files changed, 51 insertions(+), 59 deletions(-) diff --git a/lib/pangea/controllers/practice_activity_record_controller.dart b/lib/pangea/controllers/practice_activity_record_controller.dart index 45b036611..8ee52e696 100644 --- a/lib/pangea/controllers/practice_activity_record_controller.dart +++ b/lib/pangea/controllers/practice_activity_record_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; @@ -26,66 +25,60 @@ class PracticeActivityRecordController { static const int maxStoredEvents = 100; static final Map _cache = {}; late final PangeaController _pangeaController; - Timer? _cacheClearTimer; - PracticeActivityRecordController(this._pangeaController) { - _initializeCacheClearing(); - } + PracticeActivityRecordController(this._pangeaController); - LinkedHashMap get completedActivities { - try { - final dynamic locallySaved = _pangeaController.pStoreService.read( - PLocalKey.completedActivities, - ); - if (locallySaved == null) return LinkedHashMap(); - try { - final LinkedHashMap cache = - LinkedHashMap.from(locallySaved); - return cache; - } catch (err) { - _pangeaController.pStoreService.delete( - PLocalKey.completedActivities, - ); - return LinkedHashMap(); - } - } catch (exception, stackTrace) { - ErrorHandler.logError( - e: PangeaWarningError( - "Failed to get completed activities from cache: $exception", - ), - s: stackTrace, - m: 'Failed to get completed activities from cache', - ); - return LinkedHashMap(); - } + int getCompletedActivityCount(String messageID) { + return _completedActivities[messageID] ?? 0; } - Future completeActivity(String messageID) async { - final LinkedHashMap currentCache = completedActivities; - final numCompleted = currentCache[messageID] ?? 0; - currentCache[messageID] = numCompleted + 1; + final LinkedHashMap _completedActivities = + LinkedHashMap(); + + // LinkedHashMap get _completedActivities { + // try { + // final dynamic locallySaved = _pangeaController.pStoreService.read( + // PLocalKey.completedActivities, + // ); + // if (locallySaved == null) return LinkedHashMap(); + // try { + // final LinkedHashMap cache = + // LinkedHashMap.from(locallySaved); + // return cache; + // } catch (err) { + // _pangeaController.pStoreService.delete( + // PLocalKey.completedActivities, + // ); + // return LinkedHashMap(); + // } + // } catch (exception, stackTrace) { + // ErrorHandler.logError( + // e: PangeaWarningError( + // "Failed to get completed activities from cache: $exception", + // ), + // s: stackTrace, + // m: 'Failed to get completed activities from cache', + // ); + // return LinkedHashMap(); + // } + // } - if (currentCache.length > maxStoredEvents) { - currentCache.remove(currentCache.keys.first); - } - - await _pangeaController.pStoreService.save( - PLocalKey.completedActivities, - currentCache, - ); - } - - void _initializeCacheClearing() { - const duration = Duration(minutes: 2); - _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); - } - - void _clearCache() { - _cache.clear(); - } - - void dispose() { - _cacheClearTimer?.cancel(); + Future completeActivity(String messageID) async { + final numCompleted = _completedActivities[messageID] ?? 0; + _completedActivities[messageID] = numCompleted + 1; + // final LinkedHashMap currentCache = _completedActivities; + // final numCompleted = currentCache[messageID] ?? 0; + // currentCache[messageID] = numCompleted + 1; + + // if (currentCache.length > maxStoredEvents) { + // currentCache.remove(currentCache.keys.first); + // } + + // await _pangeaController.pStoreService.save( + // PLocalKey.completedActivities, + // currentCache, + // ); + debugPrint("completed activities is now: $_completedActivities"); } /// Sends a practice activity record to the server and returns the corresponding event. diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 85dfb8760..39d1bd314 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -540,8 +540,7 @@ class PangeaMessageEvent { int get numberOfActivitiesCompleted { return MatrixState.pangeaController.activityRecordController - .completedActivities[eventId] ?? - 0; + .getCompletedActivityCount(eventId); } String? get l2Code => diff --git a/pubspec.yaml b/pubspec.yaml index 43430c50c..cdb13d908 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.5+3545 +version: 1.21.6+3546 environment: sdk: ">=3.0.0 <4.0.0" From 9b97895a9d02e8b8471a70e893191e722aa56d0e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 08:50:41 -0400 Subject: [PATCH 050/115] merge main into toolbar-min-dimensions --- lib/config/app_config.dart | 2 ++ .../widgets/chat/message_audio_card.dart | 2 -- .../chat/message_selection_overlay.dart | 4 +-- lib/pangea/widgets/chat/message_toolbar.dart | 3 ++ .../chat/message_translation_card.dart | 7 +++- .../chat/message_unsubscribed_card.dart | 4 +-- .../toolbar_content_loading_indicator.dart | 32 +++++++++++-------- .../practice_activity_card.dart | 18 +++++------ 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 5a0706fe8..b044da3ee 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -23,6 +23,8 @@ abstract class AppConfig { static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; static const double toolbarMaxHeight = 300.0; + static const double toolbarMinHeight = 70.0; + static const double toolbarMinWidth = 350.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 7a003a01c..133e16b1e 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; @@ -190,7 +189,6 @@ class MessageAudioCardState extends State { children: [ Container( padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), alignment: Alignment.center, child: _isLoading ? const ToolbarContentLoadingIndicator() diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 48cdfbd47..5cf9e70b5 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -443,9 +443,7 @@ class MessageOverlayController extends State } final overlayMessage = Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - ), + constraints: BoxConstraints(maxWidth: maxWidth), child: Material( type: MaterialType.transparency, child: Column( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index bcffbdf2a..cf4192691 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -114,6 +114,9 @@ class MessageToolbar extends StatelessWidget { ), constraints: const BoxConstraints( maxHeight: AppConfig.toolbarMaxHeight, + minWidth: AppConfig.toolbarMinWidth, + minHeight: AppConfig.toolbarMinHeight, + // maxWidth is set by MessageSelectionOverlay ), child: SingleChildScrollView( child: AnimatedSize( diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 4d8bad28d..f37238a6b 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -134,26 +134,31 @@ class MessageTranslationCardState extends State { } return Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ _fetchingTranslation ? const ToolbarContentLoadingIndicator() : Flexible( child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ widget.selection != null ? selectionTranslation != null ? Text( selectionTranslation!, style: BotStyle.text(context), + textAlign: TextAlign.center, ) : const ToolbarContentLoadingIndicator() : repEvent != null ? Text( repEvent!.text, style: BotStyle.text(context), + textAlign: TextAlign.center, ) : const ToolbarContentLoadingIndicator(), if (notGoingToTranslate && widget.selection == null) diff --git a/lib/pangea/widgets/chat/message_unsubscribed_card.dart b/lib/pangea/widgets/chat/message_unsubscribed_card.dart index 5d91099b1..99a08456e 100644 --- a/lib/pangea/widgets/chat/message_unsubscribed_card.dart +++ b/lib/pangea/widgets/chat/message_unsubscribed_card.dart @@ -18,8 +18,8 @@ class MessageUnsubscribedCard extends StatelessWidget { final bool inTrialWindow = MatrixState.pangeaController.userController.inTrialWindow; - return Container( - padding: const EdgeInsets.all(8), + return Padding( + padding: const EdgeInsets.all(16), child: Column( children: [ Text( diff --git a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart index f61496013..28f15c1a9 100644 --- a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart +++ b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:flutter/material.dart'; class ToolbarContentLoadingIndicator extends StatelessWidget { @@ -8,20 +7,25 @@ class ToolbarContentLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, - child: Center( - child: SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: Theme.of(context).colorScheme.primary, - ), + return Column( + // mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], ), - ), + ], ); } } diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 7a2af7fc9..13d22890f 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -13,9 +13,9 @@ import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/content_issue_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; -import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -321,11 +321,13 @@ class PracticeActivityCardState extends State { @override Widget build(BuildContext context) { - if (!fetchingActivity && currentActivity == null) { - return GamifiedTextWidget( - userMessage: L10n.of(context)!.noActivitiesFound, - ); - } + // if (!fetchingActivity && currentActivity == null) { + // return GamifiedTextWidget( + // userMessage: L10n.of(context)!.noActivitiesFound, + // ); + // } + + return const ToolbarContentLoadingIndicator(); return Stack( alignment: Alignment.center, @@ -340,10 +342,6 @@ class PracticeActivityCardState extends State { ), // Conditionally show the darkening and progress indicator based on the loading state if (!savoringTheJoy && fetchingActivity) ...[ - // Semi-transparent overlay - Container( - color: Colors.black.withOpacity(0.5), // Darkening effect - ), // Circular progress indicator in the center const Center( child: CircularProgressIndicator(), From 59682599a5a6a3b5d3109cd44d1c10a512cae369 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 10:47:02 -0400 Subject: [PATCH 051/115] turn off choice array animation --- .../choreographer/widgets/choice_array.dart | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index 32395099e..549665064 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -1,5 +1,4 @@ import 'dart:developer'; -import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -213,98 +212,99 @@ class ChoiceAnimationWidgetState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; - AnimationState animationState = AnimationState.ready; + // AnimationState animationState = AnimationState.ready; @override void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); + // _controller = AnimationController( + // duration: const Duration(milliseconds: 300), + // vsync: this, + // ); - _animation = widget.isGold - ? Tween(begin: 1.0, end: 1.2).animate(_controller) - : TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 0, end: -8 * pi / 180), - weight: 1.0, - ), - TweenSequenceItem( - tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), - weight: 2.0, - ), - TweenSequenceItem( - tween: Tween(begin: 16 * pi / 180, end: 0), - weight: 1.0, - ), - ]).animate(_controller); + // _animation = widget.isGold + // ? Tween(begin: 1.0, end: 1.2).animate(_controller) + // : TweenSequence([ + // TweenSequenceItem( + // tween: Tween(begin: 0, end: -8 * pi / 180), + // weight: 1.0, + // ), + // TweenSequenceItem( + // tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + // weight: 2.0, + // ), + // TweenSequenceItem( + // tween: Tween(begin: 16 * pi / 180, end: 0), + // weight: 1.0, + // ), + // ]).animate(_controller); widget.enableInteraction(); - if (widget.selected && animationState == AnimationState.ready) { - widget.disableInteraction(); - _controller.forward(); - setState(() { - animationState = AnimationState.forward; - }); - } - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed && - animationState == AnimationState.forward) { - _controller.reverse(); - setState(() { - animationState = AnimationState.reverse; - }); - } - if (status == AnimationStatus.dismissed && - animationState == AnimationState.reverse) { - widget.enableInteraction(); - setState(() { - animationState = AnimationState.finished; - }); - } - }); + // if (widget.selected && animationState == AnimationState.ready) { + // widget.disableInteraction(); + // _controller.forward(); + // setState(() { + // animationState = AnimationState.forward; + // }); + // } + // _controller.addStatusListener((status) { + // if (status == AnimationStatus.completed && + // animationState == AnimationState.forward) { + // _controller.reverse(); + // setState(() { + // animationState = AnimationState.reverse; + // }); + // } + // if (status == AnimationStatus.dismissed && + // animationState == AnimationState.reverse) { + // widget.enableInteraction(); + // setState(() { + // animationState = AnimationState.finished; + // }); + // } + // }); } @override void didUpdateWidget(ChoiceAnimationWidget oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.selected && animationState == AnimationState.ready) { - widget.disableInteraction(); - _controller.forward(); - setState(() { - animationState = AnimationState.forward; - }); - } + // if (widget.selected && animationState == AnimationState.ready) { + // widget.disableInteraction(); + // _controller.forward(); + // setState(() { + // animationState = AnimationState.forward; + // }); + // } } @override Widget build(BuildContext context) { - return widget.isGold - ? AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.scale( - scale: _animation.value, - child: child, - ); - }, - child: widget.child, - ) - : AnimatedBuilder( - key: UniqueKey(), - animation: _animation, - builder: (context, child) { - return Transform.rotate( - angle: _animation.value, - child: child, - ); - }, - child: widget.child, - ); + return widget.child; + // widget.isGold + // ? AnimatedBuilder( + // key: UniqueKey(), + // animation: _animation, + // builder: (context, child) { + // return Transform.scale( + // scale: _animation.value, + // child: child, + // ); + // }, + // child: widget.child, + // ) + // : AnimatedBuilder( + // key: UniqueKey(), + // animation: _animation, + // builder: (context, child) { + // return Transform.rotate( + // angle: _animation.value, + // child: child, + // ); + // }, + // child: widget.child, + // ); } @override From 612296d341ef01c7bcd2e0e2e091a4aaa9a3570a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 10:47:31 -0400 Subject: [PATCH 052/115] bump version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index cdb13d908..98b846ae7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.6+3546 +version: 1.21.7+3547 environment: sdk: ">=3.0.0 <4.0.0" From f141680cc73ca672a08c94fb6bc29242a81db93b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:07:05 -0400 Subject: [PATCH 053/115] comment out some references to myAnalytics stream --- lib/pangea/utils/logout.dart | 4 +-- .../multiple_choice_activity.dart | 26 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/pangea/utils/logout.dart b/lib/pangea/utils/logout.dart index e2bdce074..c94cb8909 100644 --- a/lib/pangea/utils/logout.dart +++ b/lib/pangea/utils/logout.dart @@ -20,8 +20,8 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { final matrix = Matrix.of(context); // before wiping out locally cached construct data, save it to the server - await MatrixState.pangeaController.myAnalytics - .sendLocalAnalyticsToAnalyticsRoom(); + // await MatrixState.pangeaController.myAnalytics + // .sendLocalAnalyticsToAnalyticsRoom(); await showFutureLoadingDialog( context: context, diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index ee892837b..3e8a9f126 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,13 +2,11 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -68,18 +66,18 @@ class MultipleChoiceActivityState extends State { } // #freeze-activity - MatrixState.pangeaController.myAnalytics.setState( - AnalyticsStream( - // note - this maybe should be the activity event id - eventId: - widget.practiceCardController.widget.pangeaMessageEvent.eventId, - roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, - constructs: currentRecordModel!.latestResponse!.toUses( - widget.practiceCardController.currentActivity!, - widget.practiceCardController.metadata, - ), - ), - ); + // MatrixState.pangeaController.myAnalytics.setState( + // AnalyticsStream( + // // note - this maybe should be the activity event id + // eventId: + // widget.practiceCardController.widget.pangeaMessageEvent.eventId, + // roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + // constructs: currentRecordModel!.latestResponse!.toUses( + // widget.practiceCardController.currentActivity!, + // widget.practiceCardController.metadata, + // ), + // ), + // ); // If the selected choice is correct, send the record and get the next activity if (widget.currentActivity.content.isCorrect(value, index)) { From 70d598c7b2cadfbab1f334b6c388c37f3182cf4f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:07:25 -0400 Subject: [PATCH 054/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 98b846ae7..444472501 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.7+3547 +version: 1.21.8+3548 environment: sdk: ">=3.0.0 <4.0.0" From 4c51bb15f63313f424bf9e9366535b6d540a144e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:22:04 -0400 Subject: [PATCH 055/115] turn back on analytics stream call in logout --- lib/pangea/utils/logout.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pangea/utils/logout.dart b/lib/pangea/utils/logout.dart index c94cb8909..e2bdce074 100644 --- a/lib/pangea/utils/logout.dart +++ b/lib/pangea/utils/logout.dart @@ -20,8 +20,8 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { final matrix = Matrix.of(context); // before wiping out locally cached construct data, save it to the server - // await MatrixState.pangeaController.myAnalytics - // .sendLocalAnalyticsToAnalyticsRoom(); + await MatrixState.pangeaController.myAnalytics + .sendLocalAnalyticsToAnalyticsRoom(); await showFutureLoadingDialog( context: context, From 636fb150e8bac8e7b9f19b553a9432808a600fec Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:22:21 -0400 Subject: [PATCH 056/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 444472501..0a45647dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.8+3548 +version: 1.21.9+3549 environment: sdk: ">=3.0.0 <4.0.0" From 1944c19c50f1f6356a0f71db48f81051e98eaca6 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:28:20 -0400 Subject: [PATCH 057/115] Revert "when inviting tachers to analytics room, request all particpants to ensure teacher isn't already a member" This reverts commit bc1dfc1e0e8f017c9c4032afcd4b18112042d35b. --- .../pangea_room_extension/room_analytics_extension.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index b44c40ece..73371b080 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room { await analyticsRoom.requestParticipants(); } - final List participants = await analyticsRoom.requestParticipants(); + final List participants = analyticsRoom.getParticipants(); final List uninvitedTeachers = teachersLocal .where((teacher) => !participants.contains(teacher)) .toList(); @@ -110,12 +110,8 @@ extension AnalyticsRoomExtension on Room { (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { ErrorHandler.logError( e: err, - m: "Failed to invite teacher to analytics room", + m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", s: s, - data: { - "teacherId": teacher.id, - "analyticsRoomId": analyticsRoom.id, - }, ); }), ), From f6bd07400098c4a0fd9205d6ae41926af9c0ccb2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:29:02 -0400 Subject: [PATCH 058/115] revert change to getting room participants --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0a45647dc..dba848cf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.9+3549 +version: 1.22.0+3550 environment: sdk: ">=3.0.0 <4.0.0" From 5b232cea69df588ca3dbec732aad4378bb8d7bc4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:37:24 -0400 Subject: [PATCH 059/115] turn back on setState on analytics data --- .../multiple_choice_activity.dart | 26 ++++++++++--------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 3e8a9f126..ee892837b 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -2,11 +2,13 @@ import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -66,18 +68,18 @@ class MultipleChoiceActivityState extends State { } // #freeze-activity - // MatrixState.pangeaController.myAnalytics.setState( - // AnalyticsStream( - // // note - this maybe should be the activity event id - // eventId: - // widget.practiceCardController.widget.pangeaMessageEvent.eventId, - // roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, - // constructs: currentRecordModel!.latestResponse!.toUses( - // widget.practiceCardController.currentActivity!, - // widget.practiceCardController.metadata, - // ), - // ), - // ); + MatrixState.pangeaController.myAnalytics.setState( + AnalyticsStream( + // note - this maybe should be the activity event id + eventId: + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id, + constructs: currentRecordModel!.latestResponse!.toUses( + widget.practiceCardController.currentActivity!, + widget.practiceCardController.metadata, + ), + ), + ); // If the selected choice is correct, send the record and get the next activity if (widget.currentActivity.content.isCorrect(value, index)) { diff --git a/pubspec.yaml b/pubspec.yaml index dba848cf1..f1b17f029 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.0+3550 +version: 1.22.1+3551 environment: sdk: ">=3.0.0 <4.0.0" From 63cd77baf49819322ade33aa646bfd9665a192bd Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 11:58:11 -0400 Subject: [PATCH 060/115] Revert "turn off choice array animation" This reverts commit 59682599a5a6a3b5d3109cd44d1c10a512cae369. --- .../choreographer/widgets/choice_array.dart | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index 549665064..32395099e 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -212,99 +213,98 @@ class ChoiceAnimationWidgetState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; - // AnimationState animationState = AnimationState.ready; + AnimationState animationState = AnimationState.ready; @override void initState() { super.initState(); - // _controller = AnimationController( - // duration: const Duration(milliseconds: 300), - // vsync: this, - // ); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); - // _animation = widget.isGold - // ? Tween(begin: 1.0, end: 1.2).animate(_controller) - // : TweenSequence([ - // TweenSequenceItem( - // tween: Tween(begin: 0, end: -8 * pi / 180), - // weight: 1.0, - // ), - // TweenSequenceItem( - // tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), - // weight: 2.0, - // ), - // TweenSequenceItem( - // tween: Tween(begin: 16 * pi / 180, end: 0), - // weight: 1.0, - // ), - // ]).animate(_controller); + _animation = widget.isGold + ? Tween(begin: 1.0, end: 1.2).animate(_controller) + : TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: -8 * pi / 180), + weight: 1.0, + ), + TweenSequenceItem( + tween: Tween(begin: -8 * pi / 180, end: 16 * pi / 180), + weight: 2.0, + ), + TweenSequenceItem( + tween: Tween(begin: 16 * pi / 180, end: 0), + weight: 1.0, + ), + ]).animate(_controller); widget.enableInteraction(); - // if (widget.selected && animationState == AnimationState.ready) { - // widget.disableInteraction(); - // _controller.forward(); - // setState(() { - // animationState = AnimationState.forward; - // }); - // } - // _controller.addStatusListener((status) { - // if (status == AnimationStatus.completed && - // animationState == AnimationState.forward) { - // _controller.reverse(); - // setState(() { - // animationState = AnimationState.reverse; - // }); - // } - // if (status == AnimationStatus.dismissed && - // animationState == AnimationState.reverse) { - // widget.enableInteraction(); - // setState(() { - // animationState = AnimationState.finished; - // }); - // } - // }); + if (widget.selected && animationState == AnimationState.ready) { + widget.disableInteraction(); + _controller.forward(); + setState(() { + animationState = AnimationState.forward; + }); + } + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed && + animationState == AnimationState.forward) { + _controller.reverse(); + setState(() { + animationState = AnimationState.reverse; + }); + } + if (status == AnimationStatus.dismissed && + animationState == AnimationState.reverse) { + widget.enableInteraction(); + setState(() { + animationState = AnimationState.finished; + }); + } + }); } @override void didUpdateWidget(ChoiceAnimationWidget oldWidget) { super.didUpdateWidget(oldWidget); - // if (widget.selected && animationState == AnimationState.ready) { - // widget.disableInteraction(); - // _controller.forward(); - // setState(() { - // animationState = AnimationState.forward; - // }); - // } + if (widget.selected && animationState == AnimationState.ready) { + widget.disableInteraction(); + _controller.forward(); + setState(() { + animationState = AnimationState.forward; + }); + } } @override Widget build(BuildContext context) { - return widget.child; - // widget.isGold - // ? AnimatedBuilder( - // key: UniqueKey(), - // animation: _animation, - // builder: (context, child) { - // return Transform.scale( - // scale: _animation.value, - // child: child, - // ); - // }, - // child: widget.child, - // ) - // : AnimatedBuilder( - // key: UniqueKey(), - // animation: _animation, - // builder: (context, child) { - // return Transform.rotate( - // angle: _animation.value, - // child: child, - // ); - // }, - // child: widget.child, - // ); + return widget.isGold + ? AnimatedBuilder( + key: UniqueKey(), + animation: _animation, + builder: (context, child) { + return Transform.scale( + scale: _animation.value, + child: child, + ); + }, + child: widget.child, + ) + : AnimatedBuilder( + key: UniqueKey(), + animation: _animation, + builder: (context, child) { + return Transform.rotate( + angle: _animation.value, + child: child, + ); + }, + child: widget.child, + ); } @override From 6052e1618981b7fcc0f87f2412331df36fcbe75c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 12:01:05 -0400 Subject: [PATCH 061/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f1b17f029..10b124acf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.1+3551 +version: 1.22.2+3552 environment: sdk: ">=3.0.0 <4.0.0" From 23b6dd08b55f3409b94066b250c0195f620d39ce Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 12:19:00 -0400 Subject: [PATCH 062/115] when sending analytics to the server at logout, don't update the getAnalytics stream afterwards --- .../controllers/get_analytics_controller.dart | 7 ++--- .../controllers/my_analytics_controller.dart | 26 +++++++++++++++---- lib/pangea/utils/logout.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 03cb2d60b..66cf39501 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -22,7 +22,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; class GetAnalyticsController { late PangeaController _pangeaController; final List _cache = []; - StreamSubscription? _analyticsUpdateSubscription; + StreamSubscription? _analyticsUpdateSubscription; CachedStreamController> analyticsStream = CachedStreamController>(); @@ -87,8 +87,9 @@ class GetAnalyticsController { prevXP = null; } - Future onAnalyticsUpdate(AnalyticsUpdateType type) async { - if (type == AnalyticsUpdateType.server) { + Future onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async { + if (analyticsUpdate.isLogout) return; + if (analyticsUpdate.type == AnalyticsUpdateType.server) { await getConstructs(forceUpdate: true); } updateAnalyticsStream(); diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 77e6caf27..6ffa1ef31 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -21,8 +21,8 @@ enum AnalyticsUpdateType { server, local } /// 2) constructs used by the user, both in sending messages and doing practice activities class MyAnalyticsController extends BaseController { late PangeaController _pangeaController; - CachedStreamController analyticsUpdateStream = - CachedStreamController(); + CachedStreamController analyticsUpdateStream = + CachedStreamController(); StreamSubscription? _analyticsStream; Timer? _updateTimer; @@ -237,7 +237,9 @@ class MyAnalyticsController extends BaseController { final int newLevel = _pangeaController.analytics.level; newLevel > prevLevel ? sendLocalAnalyticsToAnalyticsRoom() - : analyticsUpdateStream.add(AnalyticsUpdateType.local); + : analyticsUpdateStream.add( + AnalyticsUpdate(AnalyticsUpdateType.local), + ); } /// Clears the local cache of recently sent constructs. Called before updating analytics @@ -281,7 +283,9 @@ class MyAnalyticsController extends BaseController { /// for the completion of the previous update and returns. Otherwise, it creates a new [_updateCompleter] and /// proceeds with the update process. If the update is successful, it clears any messages that were received /// since the last update and notifies the [analyticsUpdateStream]. - Future sendLocalAnalyticsToAnalyticsRoom() async { + Future sendLocalAnalyticsToAnalyticsRoom({ + onLogout = false, + }) async { if (_pangeaController.matrixState.client.userID == null) return; if (!(_updateCompleter?.isCompleted ?? true)) { await _updateCompleter!.future; @@ -293,7 +297,12 @@ class MyAnalyticsController extends BaseController { clearMessagesSinceUpdate(); lastUpdated = DateTime.now(); - analyticsUpdateStream.add(AnalyticsUpdateType.server); + analyticsUpdateStream.add( + AnalyticsUpdate( + AnalyticsUpdateType.server, + isLogout: onLogout, + ), + ); } catch (err, s) { ErrorHandler.logError( e: err, @@ -340,3 +349,10 @@ class AnalyticsStream { required this.constructs, }); } + +class AnalyticsUpdate { + final AnalyticsUpdateType type; + final bool isLogout; + + AnalyticsUpdate(this.type, {this.isLogout = false}); +} diff --git a/lib/pangea/utils/logout.dart b/lib/pangea/utils/logout.dart index e2bdce074..def632828 100644 --- a/lib/pangea/utils/logout.dart +++ b/lib/pangea/utils/logout.dart @@ -21,7 +21,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { // before wiping out locally cached construct data, save it to the server await MatrixState.pangeaController.myAnalytics - .sendLocalAnalyticsToAnalyticsRoom(); + .sendLocalAnalyticsToAnalyticsRoom(onLogout: true); await showFutureLoadingDialog( context: context, diff --git a/pubspec.yaml b/pubspec.yaml index 98b846ae7..076a0ed49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.21.7+3547 +version: 1.22.3+3553 environment: sdk: ">=3.0.0 <4.0.0" From 00d6277bc6cc1cbf4eb7d357f53d5a6ecdf3f3e1 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 24 Oct 2024 12:48:51 -0400 Subject: [PATCH 063/115] some code cleanup and comments --- lib/pangea/controllers/base_controller.dart | 8 ++++---- lib/pangea/controllers/user_controller.dart | 7 +++++++ lib/pangea/enum/construct_use_type_enum.dart | 9 +++++++++ lib/pangea/models/analytics/constructs_model.dart | 5 +---- .../message_activity_request.dart | 7 +------ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/pangea/controllers/base_controller.dart b/lib/pangea/controllers/base_controller.dart index 69939e50a..51a66dd00 100644 --- a/lib/pangea/controllers/base_controller.dart +++ b/lib/pangea/controllers/base_controller.dart @@ -1,18 +1,18 @@ import 'dart:async'; class BaseController { - final StreamController stateListener = StreamController(); + final StreamController _stateListener = StreamController(); late Stream stateStream; BaseController() { - stateStream = stateListener.stream.asBroadcastStream(); + stateStream = _stateListener.stream.asBroadcastStream(); } dispose() { - stateListener.close(); + _stateListener.close(); } setState(T data) { - stateListener.add(data); + _stateListener.add(data); } } diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index 5ee8672ff..ca7e2869a 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -121,19 +121,26 @@ class UserController extends BaseController { /// Initializes the user's profile by waiting for account data to load, reading in account /// data to profile, and migrating from the pangea profile if the account data is not present. Future _initialize() async { + // 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); diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index 1f1d37dfe..6ced270b7 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -125,3 +125,12 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { } } } + +class ConstructUseTypeUtil { + static ConstructUseTypeEnum fromString(String value) { + return ConstructUseTypeEnum.values.firstWhere( + (e) => e.string == value, + orElse: () => ConstructUseTypeEnum.nan, + ); + } +} diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 10a47516a..a9781f9ae 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -106,9 +105,7 @@ class OneConstructUse { debugger(when: kDebugMode && constructType == null); return OneConstructUse( - useType: ConstructUseTypeEnum.values - .firstWhereOrNull((e) => e.string == json['useType']) ?? - ConstructUseTypeEnum.unk, + useType: ConstructUseTypeUtil.fromString(json['useType']), lemma: json['lemma'], form: json['form'], categories: json['categories'] != null 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 0740fb8c3..9101a78ce 100644 --- a/lib/pangea/models/practice_activities.dart/message_activity_request.dart +++ b/lib/pangea/models/practice_activities.dart/message_activity_request.dart @@ -27,12 +27,7 @@ class ConstructWithXP { ? DateTime.parse(json['last_used'] as String) : null, condensedConstructUses: (json['uses'] as List).map((e) { - return ConstructUseTypeEnum.values.firstWhereOrNull( - (element) => - element.string == e || - element.toString().split('.').last == e, - ) ?? - ConstructUseTypeEnum.nan; + return ConstructUseTypeUtil.fromString(e); }).toList(), ); } From cb566d06bced6a26dd3f57d653a995378b1b71e9 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 13:24:41 -0400 Subject: [PATCH 064/115] add minimum dimensions to toolbar contents --- lib/config/app_config.dart | 2 +- lib/pages/chat/events/audio_player.dart | 2 +- lib/pangea/utils/inline_tooltip.dart | 17 +- .../widgets/chat/message_audio_card.dart | 2 + .../chat/message_speech_to_text_card.dart | 8 +- .../chat/message_translation_card.dart | 73 ++--- .../toolbar_content_loading_indicator.dart | 31 +- .../common_widgets/overlay_container.dart | 2 + lib/pangea/widgets/igc/card_error_widget.dart | 39 ++- lib/pangea/widgets/igc/card_header.dart | 37 ++- lib/pangea/widgets/igc/word_data_card.dart | 289 ++++++++---------- .../no_more_practice_card.dart | 28 +- .../practice_activity_card.dart | 61 ++-- lib/pangea/widgets/select_to_define.dart | 17 +- pubspec.yaml | 2 +- 15 files changed, 284 insertions(+), 326 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index b044da3ee..418244fb9 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -24,7 +24,7 @@ abstract class AppConfig { static const bool enableRegistration = true; static const double toolbarMaxHeight = 300.0; static const double toolbarMinHeight = 70.0; - static const double toolbarMinWidth = 350.0; + static const double toolbarMinWidth = 250.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 41e0dc388..4a4f6fac9 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -467,7 +467,7 @@ class AudioPlayerState extends State { borderRadius: BorderRadius.circular(2), ), height: 32 * (waveform[i] / 1024), - width: 1.5, + width: 3, ), ], ), diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart index bef617b98..26ec05426 100644 --- a/lib/pangea/utils/inline_tooltip.dart +++ b/lib/pangea/utils/inline_tooltip.dart @@ -30,6 +30,7 @@ class InlineTooltip extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ // Lightbulb icon on the left Icon( @@ -39,16 +40,14 @@ class InlineTooltip extends StatelessWidget { ), const SizedBox(width: 8), // Text in the middle - Expanded( - child: Center( - child: Text( - instructionsEnum.body(context), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - height: 1.5, - ), - textAlign: TextAlign.left, + Center( + child: Text( + instructionsEnum.body(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + height: 1.5, ), + textAlign: TextAlign.left, ), ), // Close button on the right diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 133e16b1e..97ffe36d5 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:math'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; @@ -208,6 +209,7 @@ class MessageAudioCardState extends State { ) : const CardErrorWidget( error: "Null audio file in message_audio_card", + maxWidth: AppConfig.toolbarMinWidth, ), ), ], diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index 92489ce29..aae3b3eeb 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; @@ -148,9 +149,12 @@ class MessageSpeechToTextCardState extends State { return const ToolbarContentLoadingIndicator(); } - //done fetchig but not results means some kind of error + // done fetchig but not results means some kind of error if (speechToTextResponse == null) { - return CardErrorWidget(error: error); + return CardErrorWidget( + error: error, + maxWidth: AppConfig.toolbarMinWidth, + ); } //TODO: find better icons diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index f37238a6b..3c0d750a3 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; @@ -130,7 +131,18 @@ class MessageTranslationCardState extends State { if (!_fetchingTranslation && repEvent == null && selectionTranslation == null) { - return const CardErrorWidget(error: "No translation found"); + return const CardErrorWidget( + error: "No translation found", + maxWidth: AppConfig.toolbarMinWidth, + ); + } + + final loadingTranslation = + (widget.selection != null && selectionTranslation == null) || + (widget.selection == null && repEvent == null); + + if (_fetchingTranslation || loadingTranslation) { + return const ToolbarContentLoadingIndicator(); } return Padding( @@ -139,42 +151,31 @@ class MessageTranslationCardState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - _fetchingTranslation - ? const ToolbarContentLoadingIndicator() - : Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.selection != null - ? selectionTranslation != null - ? Text( - selectionTranslation!, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ) - : const ToolbarContentLoadingIndicator() - : repEvent != null - ? Text( - repEvent!.text, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ) - : const ToolbarContentLoadingIndicator(), - if (notGoingToTranslate && widget.selection == null) - InlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - onClose: () => setState(() {}), - ), - if (widget.selection != null) - InlineTooltip( - instructionsEnum: - InstructionsEnum.clickAgainToDeselect, - onClose: () => setState(() {}), - ), - ], - ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.selection != null + ? selectionTranslation! + : repEvent!.text, + style: BotStyle.text(context), + textAlign: TextAlign.center, ), + if (notGoingToTranslate && widget.selection == null) + InlineTooltip( + instructionsEnum: InstructionsEnum.l1Translation, + onClose: () => setState(() {}), + ), + if (widget.selection != null) + InlineTooltip( + instructionsEnum: InstructionsEnum.clickAgainToDeselect, + onClose: () => setState(() {}), + ), + ], + ), + ), ], ), ); diff --git a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart index 28f15c1a9..a497e121d 100644 --- a/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart +++ b/lib/pangea/widgets/chat/toolbar_content_loading_indicator.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; class ToolbarContentLoadingIndicator extends StatelessWidget { @@ -7,25 +8,19 @@ class ToolbarContentLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - // mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], + return SizedBox( + width: AppConfig.toolbarMinWidth, + height: AppConfig.toolbarMinHeight, + child: Center( + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), ), - ], + ), ); } } diff --git a/lib/pangea/widgets/common_widgets/overlay_container.dart b/lib/pangea/widgets/common_widgets/overlay_container.dart index 5e2991be7..eae6c935f 100644 --- a/lib/pangea/widgets/common_widgets/overlay_container.dart +++ b/lib/pangea/widgets/common_widgets/overlay_container.dart @@ -32,6 +32,8 @@ class OverlayContainer extends StatelessWidget { constraints: BoxConstraints( maxWidth: maxWidth, maxHeight: maxHeight, + minHeight: 100, + minWidth: 100, ), //PTODO - position card above input/message // margin: const EdgeInsets.all(10), diff --git a/lib/pangea/widgets/igc/card_error_widget.dart b/lib/pangea/widgets/igc/card_error_widget.dart index 708fdd88f..52f66f5a0 100644 --- a/lib/pangea/widgets/igc/card_error_widget.dart +++ b/lib/pangea/widgets/igc/card_error_widget.dart @@ -9,37 +9,44 @@ class CardErrorWidget extends StatelessWidget { final Object? error; final Choreographer? choreographer; final int? offset; + final double? maxWidth; + const CardErrorWidget({ super.key, this.error, this.choreographer, this.offset, + this.maxWidth, }); @override Widget build(BuildContext context) { final ErrorCopy errorCopy = ErrorCopy(context, error); - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CardHeader( - text: errorCopy.title, - botExpression: BotExpression.addled, - onClose: () => choreographer?.onMatchError( - cursorOffset: offset, + return ConstrainedBox( + constraints: maxWidth != null + ? BoxConstraints(maxWidth: maxWidth!) + : const BoxConstraints(), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: errorCopy.title, + botExpression: BotExpression.addled, + onClose: () => choreographer?.onMatchError( + cursorOffset: offset, + ), ), - ), - const SizedBox(height: 10.0), - Center( - child: Text( + const SizedBox(height: 12.0), + Text( errorCopy.body, style: BotStyle.text(context), + textAlign: TextAlign.center, ), - ), - ], + ], + ), ), ); } diff --git a/lib/pangea/widgets/igc/card_header.dart b/lib/pangea/widgets/igc/card_header.dart index 671e58492..270314f72 100644 --- a/lib/pangea/widgets/igc/card_header.dart +++ b/lib/pangea/widgets/igc/card_header.dart @@ -1,8 +1,6 @@ -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:flutter/material.dart'; -import '../../../widgets/matrix.dart'; import '../common/bot_face_svg.dart'; class CardHeader extends StatelessWidget { @@ -30,26 +28,25 @@ class CardHeader extends StatelessWidget { expression: botExpression, ), ), - const SizedBox(width: 5.0), - Text( - text, - style: BotStyle.text(context), - textAlign: TextAlign.left, - ), - const SizedBox(width: 5.0), - CircleAvatar( - backgroundColor: AppConfig.primaryColor.withOpacity(0.1), - child: IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () { - if (onClose != null) onClose!(); - MatrixState.pAnyState.closeOverlay(); - }, - color: Theme.of(context).brightness == Brightness.dark - ? AppConfig.primaryColorLight - : AppConfig.primaryColor, + const SizedBox(width: 12.0), + Flexible( + child: Text( + text, + style: BotStyle.text(context), + softWrap: true, ), ), + // const SizedBox(width: 5.0), + // IconButton( + // icon: const Icon(Icons.close_outlined), + // onPressed: () { + // if (onClose != null) onClose!(); + // MatrixState.pAnyState.closeOverlay(); + // }, + // color: Theme.of(context).brightness == Brightness.dark + // ? AppConfig.primaryColorLight + // : AppConfig.primaryColor, + // ), ], ), ); diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 6f1492a75..ff494bb38 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; @@ -7,8 +8,7 @@ import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; -import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; +import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -166,71 +166,68 @@ class WordDataCardView extends StatelessWidget { @override Widget build(BuildContext context) { if (controller.wordNetError != null) { - return CardErrorWidget(error: controller.wordNetError); + return CardErrorWidget( + error: controller.wordNetError, + maxWidth: AppConfig.toolbarMinWidth, + ); } if (controller.activeL1 == null || controller.activeL2 == null) { ErrorHandler.logError(m: "should not be here"); - return CardErrorWidget(error: controller.noLanguages); + return CardErrorWidget( + error: controller.noLanguages, + maxWidth: AppConfig.toolbarMinWidth, + ); } - final ScrollController scrollController = ScrollController(); - - return Container( - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minHeight: minCardHeight), - alignment: Alignment.center, - child: Scrollbar( - thumbVisibility: true, - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (controller.widget.choiceFeedback != null) - Text( - controller.widget.choiceFeedback!, - style: BotStyle.text(context), - ), - const SizedBox(height: 5.0), - if (controller.wordData != null && - controller.wordNetError == null && - controller.activeL1 != null && - controller.activeL2 != null) - WordNetInfo( - wordData: controller.wordData!, - activeL1: controller.activeL1!, - activeL2: controller.activeL2!, - ), - if (controller.isLoadingWordNet) const PCircular(), - const SizedBox(height: 5.0), - // if (controller.widget.hasInfo && - // !controller.isLoadingContextualDefinition && - // controller.contextualDefinitionRes == null) - // Material( - // type: MaterialType.transparency, - // child: ListTile( - // leading: const BotFace( - // width: 40, expression: BotExpression.surprised), - // title: Text(L10n.of(context)!.askPangeaBot), - // onTap: controller.handleGetDefinitionButtonPress, - // ), - // ), - if (controller.isLoadingContextualDefinition) const PCircular(), - if (controller.contextualDefinitionRes != null) - Text( - controller.contextualDefinitionRes!.text, - style: BotStyle.text(context), - ), - if (controller.definitionError != null) - Text( - L10n.of(context)!.sorryNoResults, - style: BotStyle.text(context), - ), - ], - ), - ), + return Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Column( + children: [ + if (controller.widget.choiceFeedback != null) + Text( + controller.widget.choiceFeedback!, + style: BotStyle.text(context), + ), + const SizedBox(height: 5.0), + if (controller.wordData != null && + controller.wordNetError == null && + controller.activeL1 != null && + controller.activeL2 != null) + WordNetInfo( + wordData: controller.wordData!, + activeL1: controller.activeL1!, + activeL2: controller.activeL2!, + ), + if (controller.isLoadingWordNet) + const ToolbarContentLoadingIndicator(), + const SizedBox(height: 5.0), + // if (controller.widget.hasInfo && + // !controller.isLoadingContextualDefinition && + // controller.contextualDefinitionRes == null) + // Material( + // type: MaterialType.transparency, + // child: ListTile( + // leading: const BotFace( + // width: 40, expression: BotExpression.surprised), + // title: Text(L10n.of(context)!.askPangeaBot), + // onTap: controller.handleGetDefinitionButtonPress, + // ), + // ), + if (controller.isLoadingContextualDefinition) + const ToolbarContentLoadingIndicator(), + if (controller.contextualDefinitionRes != null) + Text( + controller.contextualDefinitionRes!.text, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), + if (controller.definitionError != null) + Text( + L10n.of(context)!.sorryNoResults, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), + ], ), ); } @@ -251,12 +248,14 @@ class WordNetInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SensesForLanguage( wordData: wordData, languageType: LanguageType.target, language: activeL2, ), + const SizedBox(height: 10), SensesForLanguage( wordData: wordData, languageType: LanguageType.base, @@ -273,52 +272,6 @@ enum LanguageType { } class SensesForLanguage extends StatelessWidget { - const SensesForLanguage({ - super.key, - required this.wordData, - required this.languageType, - required this.language, - }); - - final LanguageModel language; - final LanguageType languageType; - final WordData wordData; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(7, 0, 0, 0), - child: LanguageFlag( - language: language, - ), - ), - Expanded( - child: PartOfSpeechBlock( - wordData: wordData, - languageType: languageType, - ), - ), - ], - ), - ); - } -} - -class PartOfSpeechBlock extends StatelessWidget { - final WordData wordData; - final LanguageType languageType; - - const PartOfSpeechBlock({ - super.key, - required this.wordData, - required this.languageType, - }); - String get exampleSentence => languageType == LanguageType.target ? wordData.targetExampleSentence : wordData.baseExampleSentence; @@ -336,70 +289,76 @@ class PartOfSpeechBlock extends StatelessWidget { return "$word (${wordData.formattedPartOfSpeech(languageType)})"; } + const SensesForLanguage({ + super.key, + required this.wordData, + required this.languageType, + required this.language, + }); + + final LanguageModel language; + final LanguageType languageType; + final WordData wordData; + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - formattedTitle(context), - style: BotStyle.text(context, italics: true, bold: false), - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.only(left: 14.0, bottom: 10.0), - child: Align( - alignment: Alignment.centerLeft, - child: Column( - children: [ - if (definition.isNotEmpty) - RichText( - text: TextSpan( - style: BotStyle.text( - context, - italics: false, - bold: false, - ), - children: [ - TextSpan( - text: "${L10n.of(context)!.definition}: ", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan(text: definition), - ], + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LanguageFlag(language: language), + const SizedBox(width: 10), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + formattedTitle(context), + style: BotStyle.text(context, italics: true, bold: false), + ), + const SizedBox(height: 4), + if (definition.isNotEmpty) + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, + ), + children: [ + TextSpan( + text: "${L10n.of(context)!.definition}: ", + style: const TextStyle(fontWeight: FontWeight.bold), ), + TextSpan(text: definition), + ], + ), + ), + const SizedBox(height: 4), + if (exampleSentence.isNotEmpty) + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, ), - const SizedBox(height: 10), - if (exampleSentence.isNotEmpty) - RichText( - text: TextSpan( - style: BotStyle.text( - context, - italics: false, - bold: false, + children: [ + TextSpan( + text: "${L10n.of(context)!.exampleSentence}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, ), - children: [ - TextSpan( - text: "${L10n.of(context)!.exampleSentence}: ", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - TextSpan(text: exampleSentence), - ], ), - ), - ], - ), - ), + TextSpan(text: exampleSentence), + ], + ), + ), + ], ), - ], - ), + ), + ], ); } } diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart index d4844ac21..12a087d97 100644 --- a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:flutter/material.dart'; @@ -71,18 +72,21 @@ class GamifiedTextWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - const StarAnimationWidget(), - const SizedBox(height: 10), - Text( - userMessage, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - ], + return SizedBox( + width: AppConfig.toolbarMinWidth, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Column( + children: [ + const StarAnimationWidget(), + const SizedBox(height: 10), + Text( + userMessage, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), + ], + ), ), ); } diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 13d22890f..c9c42b85b 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -9,13 +9,13 @@ 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/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; -import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/content_issue_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; +import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -286,12 +286,10 @@ class PracticeActivityCardState extends State { /// If there is no current activity, the widget returns a sizedbox with a height of 80. /// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity. /// If the activity type is unknown, the widget logs an error and returns a text widget with an error message. - Widget get activityWidget { - if (currentActivity == null) { - // return sizedbox with height of 80 - return const SizedBox(height: 80); - } - switch (currentActivity!.activityType) { + Widget? get activityWidget { + switch (currentActivity?.activityType) { + case null: + return null; case ActivityTypeEnum.multipleChoice: return MultipleChoiceActivity( practiceCardController: this, @@ -304,30 +302,28 @@ class PracticeActivityCardState extends State { practiceCardController: this, currentActivity: currentActivity!, ); - default: - ErrorHandler.logError( - e: Exception('Unknown activity type'), - m: 'Unknown activity type', - data: { - 'activityType': currentActivity!.activityType, - }, - ); - return Text( - L10n.of(context)!.oopsSomethingWentWrong, - style: BotStyle.text(context), - ); + // default: + // ErrorHandler.logError( + // e: Exception('Unknown activity type'), + // m: 'Unknown activity type', + // data: { + // 'activityType': currentActivity!.activityType, + // }, + // ); + // return Text( + // L10n.of(context)!.oopsSomethingWentWrong, + // style: BotStyle.text(context), + // ); } } @override Widget build(BuildContext context) { - // if (!fetchingActivity && currentActivity == null) { - // return GamifiedTextWidget( - // userMessage: L10n.of(context)!.noActivitiesFound, - // ); - // } - - return const ToolbarContentLoadingIndicator(); + if (!fetchingActivity && currentActivity == null) { + return GamifiedTextWidget( + userMessage: L10n.of(context)!.noActivitiesFound, + ); + } return Stack( alignment: Alignment.center, @@ -336,16 +332,15 @@ class PracticeActivityCardState extends State { const Positioned( child: PointsGainedAnimation(), ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 20, 8, 8), - child: activityWidget, - ), + if (activityWidget != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: activityWidget, + ), // Conditionally show the darkening and progress indicator based on the loading state if (!savoringTheJoy && fetchingActivity) ...[ // Circular progress indicator in the center - const Center( - child: CircularProgressIndicator(), - ), + const ToolbarContentLoadingIndicator(), ], // Flag button in the top right corner Positioned( diff --git a/lib/pangea/widgets/select_to_define.dart b/lib/pangea/widgets/select_to_define.dart index 556cc7f94..968eaa147 100644 --- a/lib/pangea/widgets/select_to_define.dart +++ b/lib/pangea/widgets/select_to_define.dart @@ -10,18 +10,11 @@ class SelectToDefine extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - L10n.of(context)!.selectToDefine, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - ), - ], + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Text( + L10n.of(context)!.selectToDefine, + style: BotStyle.text(context), + textAlign: TextAlign.center, ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 076a0ed49..f8f00ee8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.3+3553 +version: 1.22.4+3554 environment: sdk: ">=3.0.0 <4.0.0" From 4b5602b2374ad360a9e2f87e4189c5867b91ed5e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 13:42:22 -0400 Subject: [PATCH 065/115] added x button back to card error header --- lib/config/app_config.dart | 2 +- lib/pangea/widgets/igc/card_error_widget.dart | 30 ++++++++-------- lib/pangea/widgets/igc/card_header.dart | 35 ++++++++++--------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 418244fb9..ab05ffd99 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -24,7 +24,7 @@ abstract class AppConfig { static const bool enableRegistration = true; static const double toolbarMaxHeight = 300.0; static const double toolbarMinHeight = 70.0; - static const double toolbarMinWidth = 250.0; + static const double toolbarMinWidth = 270.0; // #Pangea // static const Color primaryColor = Color(0xFF5625BA); // static const Color primaryColorLight = Color(0xFFCCBDEA); diff --git a/lib/pangea/widgets/igc/card_error_widget.dart b/lib/pangea/widgets/igc/card_error_widget.dart index 52f66f5a0..8c810391b 100644 --- a/lib/pangea/widgets/igc/card_error_widget.dart +++ b/lib/pangea/widgets/igc/card_error_widget.dart @@ -27,26 +27,26 @@ class CardErrorWidget extends StatelessWidget { constraints: maxWidth != null ? BoxConstraints(maxWidth: maxWidth!) : const BoxConstraints(), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CardHeader( - text: errorCopy.title, - botExpression: BotExpression.addled, - onClose: () => choreographer?.onMatchError( - cursorOffset: offset, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: errorCopy.title, + botExpression: BotExpression.addled, + onClose: () => choreographer?.onMatchError( + cursorOffset: offset, ), - const SizedBox(height: 12.0), - Text( + ), + const SizedBox(height: 12.0), + Padding( + padding: const EdgeInsets.all(12), + child: Text( errorCopy.body, style: BotStyle.text(context), textAlign: TextAlign.center, ), - ], - ), + ), + ], ), ); } diff --git a/lib/pangea/widgets/igc/card_header.dart b/lib/pangea/widgets/igc/card_header.dart index 270314f72..1816cc214 100644 --- a/lib/pangea/widgets/igc/card_header.dart +++ b/lib/pangea/widgets/igc/card_header.dart @@ -1,4 +1,6 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import '../common/bot_face_svg.dart'; @@ -20,13 +22,12 @@ class CardHeader extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 5.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: BotFace( - width: 50.0, - expression: botExpression, - ), + BotFace( + width: 50.0, + expression: botExpression, ), const SizedBox(width: 12.0), Flexible( @@ -36,17 +37,17 @@ class CardHeader extends StatelessWidget { softWrap: true, ), ), - // const SizedBox(width: 5.0), - // IconButton( - // icon: const Icon(Icons.close_outlined), - // onPressed: () { - // if (onClose != null) onClose!(); - // MatrixState.pAnyState.closeOverlay(); - // }, - // color: Theme.of(context).brightness == Brightness.dark - // ? AppConfig.primaryColorLight - // : AppConfig.primaryColor, - // ), + const SizedBox(width: 5.0), + IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () { + if (onClose != null) onClose!(); + MatrixState.pAnyState.closeOverlay(); + }, + color: Theme.of(context).brightness == Brightness.dark + ? AppConfig.primaryColorLight + : AppConfig.primaryColor, + ), ], ), ); From dc79a50fda7606140a485bcf67b29794c27c8ab0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 13:44:32 -0400 Subject: [PATCH 066/115] only init one instance of ttscontroller, don't stop tts twice --- .../widgets/chat/message_audio_card.dart | 8 +++---- .../chat/message_selection_overlay.dart | 8 +++++-- lib/pangea/widgets/chat/message_toolbar.dart | 5 ++++ lib/pangea/widgets/chat/tts_controller.dart | 5 ++-- .../multiple_choice_activity.dart | 8 ++++++- .../practice_activity_card.dart | 5 ++++ .../practice_activity/word_audio_button.dart | 24 ++++--------------- pubspec.yaml | 6 ++--- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 97ffe36d5..e66e69477 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -21,11 +21,13 @@ class MessageAudioCard extends StatefulWidget { final PangeaMessageEvent messageEvent; final MessageOverlayController overlayController; final PangeaTokenText? selection; + final TtsController tts; const MessageAudioCard({ super.key, required this.messageEvent, required this.overlayController, + required this.tts, this.selection, }); @@ -40,8 +42,6 @@ class MessageAudioCardState extends State { int? sectionStartMS; int? sectionEndMS; - TtsController tts = TtsController(); - @override void initState() { super.initState(); @@ -68,7 +68,7 @@ class MessageAudioCardState extends State { final PangeaTokenText selection = widget.selection!; final tokenText = selection.content; - await tts.speak(tokenText); + await widget.tts.speak(tokenText); } void setSectionStartAndEnd(int? start, int? end) => mounted @@ -204,7 +204,7 @@ class MessageAudioCardState extends State { color: Theme.of(context).colorScheme.onPrimaryContainer, ), - tts.missingVoiceButton, + widget.tts.missingVoiceButton, ], ) : const CardErrorWidget( diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 5cf9e70b5..dbce6f2b8 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -61,11 +62,11 @@ class MessageOverlayController extends State /// The number of activities that need to be completed before the toolbar is unlocked /// If we don't have any good activities for them, we'll decrease this number static const int neededActivities = 3; - int activitiesLeftToComplete = neededActivities; - PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent; + final TtsController tts = TtsController(); + @override void initState() { super.initState(); @@ -98,6 +99,7 @@ class MessageOverlayController extends State ).listen((_) => setState(() {})); setInitialToolbarMode(); + tts.setupTTS(); } /// We need to check if the setState call is safe to call immediately @@ -359,6 +361,7 @@ class MessageOverlayController extends State void dispose() { _animationController.dispose(); _reactionSubscription?.cancel(); + tts.dispose(); super.dispose(); } @@ -455,6 +458,7 @@ class MessageOverlayController extends State MessageToolbar( pangeaMessageEvent: widget._pangeaMessageEvent, overLayController: this, + tts: tts, ), SizedBox( height: adjustedMessageHeight, diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index cf4192691..2f2f2b736 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/select_to_define.dart'; @@ -22,11 +23,13 @@ const double minCardHeight = 70; class MessageToolbar extends StatelessWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overLayController; + final TtsController tts; const MessageToolbar({ super.key, required this.pangeaMessageEvent, required this.overLayController, + required this.tts, }); Widget get toolbarContent { @@ -50,6 +53,7 @@ class MessageToolbar extends StatelessWidget { messageEvent: pangeaMessageEvent, overlayController: overLayController, selection: overLayController.selectedSpan, + tts: tts, ); case MessageMode.speechToText: return MessageSpeechToTextCard( @@ -87,6 +91,7 @@ class MessageToolbar extends StatelessWidget { return PracticeActivityCard( pangeaMessageEvent: pangeaMessageEvent, overlayController: overLayController, + tts: tts, ); default: debugger(when: kDebugMode); diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 225d7a04e..c98cb6220 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -23,7 +23,8 @@ class TtsController { } onError(dynamic message) => ErrorHandler.logError( - m: 'TTS error', + e: message, + m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error', data: { 'message': message, }, @@ -82,13 +83,11 @@ class TtsController { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s); } - await tts.stop(); } Future speak(String text) async { try { stop(); - targetLanguage ??= MatrixState.pangeaController.languageController.userL2?.langCode; diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index ee892837b..e76021000 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -16,11 +17,13 @@ import 'package:flutter/material.dart'; class MultipleChoiceActivity extends StatefulWidget { final PracticeActivityCardState practiceCardController; final PracticeActivityModel currentActivity; + final TtsController tts; const MultipleChoiceActivity({ super.key, required this.practiceCardController, required this.currentActivity, + required this.tts, }); @override @@ -112,7 +115,10 @@ class MultipleChoiceActivityState extends State { // #freeze-activity if (practiceActivity.activityType == ActivityTypeEnum.wordFocusListening) - WordAudioButton(text: practiceActivity.content.answer), + WordAudioButton( + text: practiceActivity.content.answer, + ttsController: widget.tts, + ), ChoicesArray( isLoading: false, uniqueKeyForLayerLink: (index) => "multiple_choice_$index", diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index c9c42b85b..dbb98de73 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; +import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/content_issue_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; @@ -28,11 +29,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class PracticeActivityCard extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overlayController; + final TtsController tts; const PracticeActivityCard({ super.key, required this.pangeaMessageEvent, required this.overlayController, + required this.tts, }); @override @@ -294,6 +297,7 @@ class PracticeActivityCardState extends State { return MultipleChoiceActivity( practiceCardController: this, currentActivity: currentActivity!, + tts: widget.tts, ); case ActivityTypeEnum.wordFocusListening: // return WordFocusListeningActivity( @@ -301,6 +305,7 @@ class PracticeActivityCardState extends State { return MultipleChoiceActivity( practiceCardController: this, currentActivity: currentActivity!, + tts: widget.tts, ); // default: // ErrorHandler.logError( diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 24835fcb2..2f56299c8 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -4,10 +4,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class WordAudioButton extends StatefulWidget { final String text; + final TtsController ttsController; const WordAudioButton({ super.key, required this.text, + required this.ttsController, }); @override @@ -17,22 +19,6 @@ class WordAudioButton extends StatefulWidget { class WordAudioButtonState extends State { bool _isPlaying = false; - TtsController ttsController = TtsController(); - - @override - void initState() { - // TODO: implement initState - debugPrint('initState WordAudioButton'); - super.initState(); - ttsController.setupTTS().then((value) => setState(() {})); - } - - @override - void dispose() { - ttsController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { debugPrint('build WordAudioButton'); @@ -54,7 +40,7 @@ class WordAudioButtonState extends State { _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, onPressed: () async { if (_isPlaying) { - await ttsController.tts.stop(); + await widget.ttsController.tts.stop(); if (mounted) { setState(() => _isPlaying = false); } @@ -62,7 +48,7 @@ class WordAudioButtonState extends State { if (mounted) { setState(() => _isPlaying = true); } - await ttsController.speak(widget.text); + await widget.ttsController.speak(widget.text); if (mounted) { setState(() => _isPlaying = false); } @@ -70,7 +56,7 @@ class WordAudioButtonState extends State { }, // Disable button if language isn't supported ), // #freeze-activity - ttsController.missingVoiceButton, + widget.ttsController.missingVoiceButton, ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index f8f00ee8f..25403c0ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.4+3554 +version: 1.22.5+3555 environment: sdk: ">=3.0.0 <4.0.0" @@ -162,8 +162,8 @@ flutter: # #Pangea # uncomment this to enable mobile builds # causes error with github actions - # - .env - # - assets/.env + - .env + - assets/.env - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From a3845eb13ab33ba085e50041d25ddfb6da66db10 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 13:46:20 -0400 Subject: [PATCH 067/115] comment out env file paths in pubspec --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 25403c0ec..377087387 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -162,8 +162,8 @@ flutter: # #Pangea # uncomment this to enable mobile builds # causes error with github actions - - .env - - assets/.env + # - .env + # - assets/.env - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From 9e3111f97c68c347cc5aa67cf6df197e3a63bb0a Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 14:16:39 -0400 Subject: [PATCH 068/115] don't play token text if null message audio is playing --- lib/pages/chat/events/audio_player.dart | 11 +++++++++-- lib/pangea/widgets/chat/message_audio_card.dart | 6 +++++- .../widgets/chat/message_selection_overlay.dart | 15 ++++++++++++--- lib/pangea/widgets/chat/message_toolbar.dart | 1 + pubspec.yaml | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 4a4f6fac9..c79fc9cb3 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -21,6 +21,7 @@ class AudioPlayerWidget extends StatefulWidget { final Event? event; final PangeaAudioFile? matrixFile; final bool autoplay; + final Function(bool)? setIsPlayingAudio; // Pangea# static String? currentId; @@ -41,6 +42,7 @@ class AudioPlayerWidget extends StatefulWidget { this.autoplay = false, this.sectionStartMS, this.sectionEndMS, + this.setIsPlayingAudio, // Pangea# super.key, }); @@ -204,8 +206,13 @@ class AudioPlayerState extends State { if (max == null || max == Duration.zero) return; setState(() => maxPosition = max.inMilliseconds.toDouble()); }); - onPlayerStateChanged ??= - audioPlayer.playingStream.listen((_) => setState(() {})); + onPlayerStateChanged ??= audioPlayer.playingStream.listen( + (isPlaying) => setState(() { + // #Pangea + widget.setIsPlayingAudio?.call(isPlaying); + // Pangea# + }), + ); final audioFile = this.audioFile; if (audioFile != null) { audioPlayer.setFilePath(audioFile.path); diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index e66e69477..47cb41af8 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -22,12 +22,14 @@ class MessageAudioCard extends StatefulWidget { final MessageOverlayController overlayController; final PangeaTokenText? selection; final TtsController tts; + final Function(bool) setIsPlayingAudio; const MessageAudioCard({ super.key, required this.messageEvent, required this.overlayController, required this.tts, + required this.setIsPlayingAudio, this.selection, }); @@ -56,7 +58,7 @@ class MessageAudioCardState extends State { @override void didUpdateWidget(covariant oldWidget) { - if (oldWidget.selection != widget.selection) { + if (oldWidget.selection != widget.selection && widget.selection != null) { debugPrint('selection changed'); setSectionStartAndEndFromSelection(); playSelectionAudio(); @@ -65,6 +67,7 @@ class MessageAudioCardState extends State { } Future playSelectionAudio() async { + if (widget.selection == null) return; final PangeaTokenText selection = widget.selection!; final tokenText = selection.content; @@ -203,6 +206,7 @@ class MessageAudioCardState extends State { sectionEndMS: sectionEndMS, color: Theme.of(context).colorScheme.onPrimaryContainer, + setIsPlayingAudio: widget.setIsPlayingAudio, ), widget.tts.missingVoiceButton, ], diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index dbce6f2b8..a8022b1cc 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -66,6 +66,7 @@ class MessageOverlayController extends State PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent; final TtsController tts = TtsController(); + bool isPlayingAudio = false; @override void initState() { @@ -200,9 +201,10 @@ class MessageOverlayController extends State PangeaToken token, ) { if ([ - MessageMode.practiceActivity, - // MessageMode.textToSpeech - ].contains(toolbarMode)) { + MessageMode.practiceActivity, + // MessageMode.textToSpeech + ].contains(toolbarMode) || + isPlayingAudio) { return; } @@ -273,6 +275,13 @@ class MessageOverlayController extends State double get reactionsHeight => hasReactions ? 28 : 0; double get belowMessageHeight => toolbarButtonsHeight + reactionsHeight; + void setIsPlayingAudio(bool isPlaying) { + if (mounted) { + setState(() => isPlayingAudio = isPlaying); + debugPrint("IS PLAYING AUDIO: $isPlaying"); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 2f2f2b736..071a99f69 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -54,6 +54,7 @@ class MessageToolbar extends StatelessWidget { overlayController: overLayController, selection: overLayController.selectedSpan, tts: tts, + setIsPlayingAudio: overLayController.setIsPlayingAudio, ); case MessageMode.speechToText: return MessageSpeechToTextCard( diff --git a/pubspec.yaml b/pubspec.yaml index 377087387..80906d77c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.5+3555 +version: 1.22.6+3556 environment: sdk: ">=3.0.0 <4.0.0" From 1a151e90b7ec7d82f012c3b10a43fe2a98be7aac Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 14:33:25 -0400 Subject: [PATCH 069/115] removed print statement --- lib/pangea/widgets/chat/message_selection_overlay.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index a8022b1cc..bd5b3da00 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -278,7 +278,6 @@ class MessageOverlayController extends State void setIsPlayingAudio(bool isPlaying) { if (mounted) { setState(() => isPlayingAudio = isPlaying); - debugPrint("IS PLAYING AUDIO: $isPlaying"); } } From fbccd3a7f87a4a2a593042205f7c33e35e32c7b3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 24 Oct 2024 14:55:40 -0400 Subject: [PATCH 070/115] some tweaks to the card header --- lib/pangea/widgets/igc/card_header.dart | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/pangea/widgets/igc/card_header.dart b/lib/pangea/widgets/igc/card_header.dart index 1816cc214..0063c9638 100644 --- a/lib/pangea/widgets/igc/card_header.dart +++ b/lib/pangea/widgets/igc/card_header.dart @@ -22,19 +22,24 @@ class CardHeader extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 5.0), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - BotFace( - width: 50.0, - expression: botExpression, - ), - const SizedBox(width: 12.0), Flexible( - child: Text( - text, - style: BotStyle.text(context), - softWrap: true, + child: Row( + children: [ + BotFace( + width: 50.0, + expression: botExpression, + ), + const SizedBox(width: 12.0), + Flexible( + child: Text( + text, + style: BotStyle.text(context), + softWrap: true, + ), + ), + ], ), ), const SizedBox(width: 5.0), From cd50460939a49084f28ce0e9bae63a49cd44a99b Mon Sep 17 00:00:00 2001 From: choreo development Date: Thu, 24 Oct 2024 16:36:29 -0400 Subject: [PATCH 071/115] got rid of practice activity for non target language --- lib/pangea/enum/message_mode_enum.dart | 10 +++- .../widgets/chat/message_display_card.dart | 47 +++++++++++++++++++ .../chat/message_selection_overlay.dart | 9 ++++ lib/pangea/widgets/chat/message_toolbar.dart | 16 +++++++ .../widgets/chat/message_toolbar_buttons.dart | 2 +- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 lib/pangea/widgets/chat/message_display_card.dart diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index c8659f0fc..544e2a270 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -9,6 +9,7 @@ enum MessageMode { definition, translation, speechToText, + nullMode } extension MessageModeExtension on MessageMode { @@ -25,6 +26,7 @@ extension MessageModeExtension on MessageMode { return Icons.book; case MessageMode.practiceActivity: return Symbols.fitness_center; + case MessageMode.nullMode: default: return Icons.error; // Icon to indicate an error or unsupported mode } @@ -42,6 +44,7 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.definitions; case MessageMode.practiceActivity: return L10n.of(context)!.practice; + case MessageMode.nullMode: default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode @@ -60,18 +63,23 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.define; case MessageMode.practiceActivity: return L10n.of(context)!.practice; + case MessageMode.nullMode: default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode } } - bool isValidMode(Event event) { + bool shouldShowAsToolbarButton(Event event) { switch (this) { case MessageMode.translation: + return event.messageType == MessageTypes.Text; case MessageMode.textToSpeech: + return event.messageType == MessageTypes.Text; case MessageMode.definition: return event.messageType == MessageTypes.Text; + case MessageMode.nullMode: + return false; case MessageMode.speechToText: return event.messageType == MessageTypes.Audio; case MessageMode.practiceActivity: diff --git a/lib/pangea/widgets/chat/message_display_card.dart b/lib/pangea/widgets/chat/message_display_card.dart new file mode 100644 index 000000000..9deba5d27 --- /dev/null +++ b/lib/pangea/widgets/chat/message_display_card.dart @@ -0,0 +1,47 @@ +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:flutter/material.dart'; + +class MessageDisplayCard extends StatelessWidget { + final PangeaMessageEvent messageEvent; + final String? displayText; + + const MessageDisplayCard({ + super.key, + required this.messageEvent, + required this.displayText, + }); + + @override + Widget build(BuildContext context) { + // If no display text is provided, show a message indicating no content + if (displayText == null || displayText!.isEmpty) { + return const Center( + child: Text( + 'No content available.', + style: TextStyle(color: Colors.black54, fontSize: 16), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + children: [ + // Display the provided text + Text( + displayText!, + style: BotStyle.text(context), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index db421dd15..507c6c000 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -63,6 +63,10 @@ class MessageOverlayController extends State int activitiesLeftToComplete = neededActivities; + bool get messageInUserL2 => + pangeaMessageEvent.messageDisplayLangCode == + MatrixState.pangeaController.languageController.userL2?.langCode; + PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent; @override @@ -143,6 +147,11 @@ class MessageOverlayController extends State toolbarMode = MessageMode.speechToText; return; } + // if (!messageInUserL2) { + // activitiesLeftToComplete = 0; + // toolbarMode = MessageMode.nullMode; + // return; + // } if (activitiesLeftToComplete > 0) { toolbarMode = MessageMode.practiceActivity; diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index bcffbdf2a..dceffdf7f 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_display_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; @@ -39,7 +40,22 @@ class MessageToolbar extends StatelessWidget { ); } + // Check if the message is in the user's second language + final bool messageInUserL2 = pangeaMessageEvent.messageDisplayLangCode == + MatrixState.pangeaController.languageController.userL2?.langCode; + + // If not in the target language, set to nullMode + if (!messageInUserL2) { + overLayController.toolbarMode = MessageMode.nullMode; + } + switch (overLayController.toolbarMode) { + case MessageMode.nullMode: + return MessageDisplayCard( + messageEvent: pangeaMessageEvent, // Pass the message event here + displayText: + "Message not in target language", // Pass the display text, + ); case MessageMode.translation: return MessageTranslationCard( messageEvent: pangeaMessageEvent, diff --git a/lib/pangea/widgets/chat/message_toolbar_buttons.dart b/lib/pangea/widgets/chat/message_toolbar_buttons.dart index 41cd47c6a..190a0fdff 100644 --- a/lib/pangea/widgets/chat/message_toolbar_buttons.dart +++ b/lib/pangea/widgets/chat/message_toolbar_buttons.dart @@ -22,7 +22,7 @@ class ToolbarButtons extends StatelessWidget { overlayController.pangeaMessageEvent; List get modes => MessageMode.values - .where((mode) => mode.isValidMode(pangeaMessageEvent.event)) + .where((mode) => mode.shouldShowAsToolbarButton(pangeaMessageEvent.event)) .toList(); static const double iconWidth = 36.0; From fa8526d58d8d901927a579edc3b1d809c11ec642 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 09:15:12 -0400 Subject: [PATCH 072/115] added copy to arb file, added padding to widget to show text in toolbar --- assets/l10n/intl_en.arb | 3 +- lib/pangea/enum/message_mode_enum.dart | 6 --- .../widgets/chat/message_display_card.dart | 47 ------------------- lib/pangea/widgets/chat/message_toolbar.dart | 23 +++++---- ..._define.dart => message_display_card.dart} | 10 ++-- pubspec.yaml | 2 +- 6 files changed, 20 insertions(+), 71 deletions(-) delete mode 100644 lib/pangea/widgets/chat/message_display_card.dart rename lib/pangea/widgets/{select_to_define.dart => message_display_card.dart} (69%) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a9f2f1db0..c54d4e7b0 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4365,5 +4365,6 @@ "chooseVoice": "Choose a voice", "enterLanguageLevel": "Please enter a language level", "enterDiscussionTopic": "Please enter a discussion topic", - "selectBotChatMode": "Select chat mode" + "selectBotChatMode": "Select chat mode", + "messageNotInTargetLang": "Message not in target language" } \ No newline at end of file diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index 544e2a270..cfc42f63b 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -9,7 +9,6 @@ enum MessageMode { definition, translation, speechToText, - nullMode } extension MessageModeExtension on MessageMode { @@ -26,7 +25,6 @@ extension MessageModeExtension on MessageMode { return Icons.book; case MessageMode.practiceActivity: return Symbols.fitness_center; - case MessageMode.nullMode: default: return Icons.error; // Icon to indicate an error or unsupported mode } @@ -44,7 +42,6 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.definitions; case MessageMode.practiceActivity: return L10n.of(context)!.practice; - case MessageMode.nullMode: default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode @@ -63,7 +60,6 @@ extension MessageModeExtension on MessageMode { return L10n.of(context)!.define; case MessageMode.practiceActivity: return L10n.of(context)!.practice; - case MessageMode.nullMode: default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode @@ -78,8 +74,6 @@ extension MessageModeExtension on MessageMode { return event.messageType == MessageTypes.Text; case MessageMode.definition: return event.messageType == MessageTypes.Text; - case MessageMode.nullMode: - return false; case MessageMode.speechToText: return event.messageType == MessageTypes.Audio; case MessageMode.practiceActivity: diff --git a/lib/pangea/widgets/chat/message_display_card.dart b/lib/pangea/widgets/chat/message_display_card.dart deleted file mode 100644 index 9deba5d27..000000000 --- a/lib/pangea/widgets/chat/message_display_card.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/utils/bot_style.dart'; -import 'package:flutter/material.dart'; - -class MessageDisplayCard extends StatelessWidget { - final PangeaMessageEvent messageEvent; - final String? displayText; - - const MessageDisplayCard({ - super.key, - required this.messageEvent, - required this.displayText, - }); - - @override - Widget build(BuildContext context) { - // If no display text is provided, show a message indicating no content - if (displayText == null || displayText!.isEmpty) { - return const Center( - child: Text( - 'No content available.', - style: TextStyle(color: Colors.black54, fontSize: 16), - ), - ); - } - - return Padding( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Column( - children: [ - // Display the provided text - Text( - displayText!, - style: BotStyle.text(context), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index b17bf78a2..36b3cc45c 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -6,18 +6,18 @@ import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_display_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/pangea/widgets/message_display_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; -import 'package:fluffychat/pangea/widgets/select_to_define.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'; const double minCardHeight = 70; @@ -33,7 +33,7 @@ class MessageToolbar extends StatelessWidget { required this.tts, }); - Widget get toolbarContent { + Widget toolbarContent(BuildContext context) { final bool subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; @@ -49,16 +49,13 @@ class MessageToolbar extends StatelessWidget { // If not in the target language, set to nullMode if (!messageInUserL2) { - overLayController.toolbarMode = MessageMode.nullMode; + return MessageDisplayCard( + displayText: + L10n.of(context)!.messageNotInTargetLang, // Pass the display text, + ); } switch (overLayController.toolbarMode) { - case MessageMode.nullMode: - return MessageDisplayCard( - messageEvent: pangeaMessageEvent, // Pass the message event here - displayText: - "Message not in target language", // Pass the display text, - ); case MessageMode.translation: return MessageTranslationCard( messageEvent: pangeaMessageEvent, @@ -78,7 +75,9 @@ class MessageToolbar extends StatelessWidget { ); case MessageMode.definition: if (!overLayController.isSelection) { - return const SelectToDefine(); + return MessageDisplayCard( + displayText: L10n.of(context)!.selectToDefine, + ); } else { try { final selectedText = overLayController.targetText; @@ -143,7 +142,7 @@ class MessageToolbar extends StatelessWidget { child: SingleChildScrollView( child: AnimatedSize( duration: FluffyThemes.animationDuration, - child: toolbarContent, + child: toolbarContent(context), ), ), ); diff --git a/lib/pangea/widgets/select_to_define.dart b/lib/pangea/widgets/message_display_card.dart similarity index 69% rename from lib/pangea/widgets/select_to_define.dart rename to lib/pangea/widgets/message_display_card.dart index 968eaa147..8536f2c99 100644 --- a/lib/pangea/widgets/select_to_define.dart +++ b/lib/pangea/widgets/message_display_card.dart @@ -1,10 +1,12 @@ import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -class SelectToDefine extends StatelessWidget { - const SelectToDefine({ +class MessageDisplayCard extends StatelessWidget { + final String displayText; + + const MessageDisplayCard({ super.key, + required this.displayText, }); @override @@ -12,7 +14,7 @@ class SelectToDefine extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), child: Text( - L10n.of(context)!.selectToDefine, + displayText, style: BotStyle.text(context), textAlign: TextAlign.center, ), diff --git a/pubspec.yaml b/pubspec.yaml index 80906d77c..2ae643555 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.6+3556 +version: 1.22.7+3557 environment: sdk: ">=3.0.0 <4.0.0" From cf1f79147a601d007ec7d45d07160e17f82a8790 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 09:20:50 -0400 Subject: [PATCH 073/115] don't allow users to edit the input bar during IT --- lib/pages/chat/chat.dart | 2 +- lib/pages/chat/chat_input_row.dart | 1 + lib/pages/chat/input_bar.dart | 2 ++ lib/pangea/choreographer/controllers/choreographer.dart | 9 +++++---- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index cb1c86f5e..ebfd384bc 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1419,7 +1419,7 @@ class ChatController extends State void onSelectMessage(Event event) { // #Pangea - if (choreographer.itController.isOpen) { + if (choreographer.itController.willOpen) { return; } // Pangea# diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index cc931ef12..57360d25d 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -321,6 +321,7 @@ class ChatInputRow extends StatelessWidget { // #Pangea // hintText: L10n.of(context)!.writeAMessage, hintText: hintText(), + disabledBorder: InputBorder.none, // Pangea# hintMaxLines: 1, border: InputBorder.none, diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index c0939f278..347d68786 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -478,6 +478,8 @@ class InputBar extends StatelessWidget { // builder: (context, controller, focusNode) => TextField( builder: (context, _, focusNode) => TextField( enableSuggestions: false, + readOnly: + controller != null && controller!.choreographer.isRunningIT, // Pangea# controller: controller, focusNode: focusNode, diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index df254d49c..9d1691620 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -261,7 +261,7 @@ class Choreographer { // debugger(when: kDebugMode); } - await (choreoMode == ChoreoMode.it && !itController.isTranslationDone + await (isRunningIT ? itController.getTranslationData(_useCustomInput) : igc.getIGCTextData( onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection, @@ -418,7 +418,7 @@ class Choreographer { setState(); } - giveInputFocus() { + void giveInputFocus() { Future.delayed(Duration.zero, () { chatController.inputFocus.requestFocus(); }); @@ -478,6 +478,9 @@ class Choreographer { bool get _noChange => _lastChecked != null && _lastChecked == _textController.text; + bool get isRunningIT => + choreoMode == ChoreoMode.it && !itController.isTranslationDone; + void startLoading() { _lastChecked = _textController.text; isFetching = true; @@ -505,8 +508,6 @@ class Choreographer { } } - bool get showIsError => !itController.isOpen && errorService.isError; - LayerLinkAndKey get itBarLinkAndKey => MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey); From 49588b91cfe4fbaf88b4e927fa179b618956566c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 09:44:18 -0400 Subject: [PATCH 074/115] don't allow send + make send button red while running IT --- .../controllers/choreographer.dart | 22 ++++++++++++++++--- .../controllers/igc_controller.dart | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 9d1691620..3d39a308b 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -68,7 +68,7 @@ class Choreographer { } void send(BuildContext context) { - if (isFetching) return; + if (!canSendMessage) return; if (pangeaController.subscriptionController.subscriptionStatus == SubscriptionStatus.showPaywall) { @@ -92,7 +92,7 @@ class Choreographer { } Future _sendWithIGC(BuildContext context) async { - if (!igc.canSendMessage) { + if (!canSendMessage) { igc.showFirstMatch(context); return; } @@ -571,7 +571,7 @@ class Choreographer { return AssistanceState.noMessage; } - if (igc.igcTextData?.matches.isNotEmpty ?? false) { + if ((igc.igcTextData?.matches.isNotEmpty ?? false) || isRunningIT) { return AssistanceState.fetched; } @@ -585,4 +585,20 @@ class Choreographer { return AssistanceState.complete; } + + bool get canSendMessage { + if (isFetching) return false; + if (errorService.isError) return true; + if (itEnabled && isRunningIT) return false; + + final hasITMatches = + igc.igcTextData!.matches.any((match) => match.isITStart); + final hasIGCMatches = + igc.igcTextData!.matches.any((match) => !match.isITStart); + + if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) { + return false; + } + return true; + } } diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 1acb37332..2b66dbd5d 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -202,8 +202,8 @@ class IgcController { } return !((choreographer.itEnabled && - igcTextData!.matches.any((match) => match.isOutOfTargetMatch)) || + igcTextData!.matches.any((match) => match.isITStart)) || (choreographer.igcEnabled && - igcTextData!.matches.any((match) => !match.isOutOfTargetMatch))); + igcTextData!.matches.any((match) => !match.isITStart))); } } From f4c0637f83d69f335e722a2456b42eaad4973ab5 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 09:49:53 -0400 Subject: [PATCH 075/115] only reset IT source text if the user hasn't gone through any IT steps --- lib/pangea/choreographer/controllers/it_controller.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index b618386f8..e30f30b3d 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -68,9 +68,10 @@ class ITController { } void closeIT() { - //if they close it before completing, just put their text back - //PTODO - explore using last itStep - choreographer.textController.text = sourceText ?? ""; + // if the user hasn't gone through any IT steps, reset the text + if (completedITSteps.isEmpty && sourceText != null) { + choreographer.textController.text = sourceText!; + } clear(); } From 01f07c558426a201891bf74a48e2c10dbab1b99e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 09:51:39 -0400 Subject: [PATCH 076/115] move canSend function from igcController to choreographer --- .../choreographer/controllers/igc_controller.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 2b66dbd5d..ed770cca4 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -192,18 +192,4 @@ class IgcController { // Not sure why this is here // MatrixState.pAnyState.closeOverlay(); } - - bool get canSendMessage { - if (choreographer.isFetching) return false; - if (igcTextData == null || - choreographer.errorService.isError || - igcTextData!.matches.isEmpty) { - return true; - } - - return !((choreographer.itEnabled && - igcTextData!.matches.any((match) => match.isITStart)) || - (choreographer.igcEnabled && - igcTextData!.matches.any((match) => !match.isITStart))); - } } From 380689cf03d39e48510372598f1d02d785355e38 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 10:04:59 -0400 Subject: [PATCH 077/115] if running language assistance after going through IT, clear the itController --- lib/pangea/choreographer/controllers/choreographer.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 3d39a308b..f3e48a8b6 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -255,10 +255,13 @@ class Choreographer { } startLoading(); + + // if getting language assistance after finishing IT, + // reset the itController if (choreoMode == ChoreoMode.it && itController.isTranslationDone && !onlyTokensAndLanguageDetection) { - // debugger(when: kDebugMode); + itController.clear(); } await (isRunningIT From 4c3fccd55f960538f534f1d5a2a7a6f1003c2893 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 10:08:13 -0400 Subject: [PATCH 078/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2ae643555..e24350240 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.7+3557 +version: 1.22.8+3558 environment: sdk: ">=3.0.0 <4.0.0" From 54975adbb33991d5513478e7e3f3ae66c6cb1bbd Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 10:13:15 -0400 Subject: [PATCH 079/115] require non-null error in card error widget --- lib/pangea/choreographer/widgets/it_feedback_card.dart | 2 +- lib/pangea/widgets/chat/message_speech_to_text_card.dart | 2 +- lib/pangea/widgets/igc/card_error_widget.dart | 4 ++-- lib/pangea/widgets/igc/word_data_card.dart | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pangea/choreographer/widgets/it_feedback_card.dart b/lib/pangea/choreographer/widgets/it_feedback_card.dart index dd72f2457..f006e2552 100644 --- a/lib/pangea/choreographer/widgets/it_feedback_card.dart +++ b/lib/pangea/choreographer/widgets/it_feedback_card.dart @@ -103,7 +103,7 @@ class ITFeedbackCardController extends State { @override Widget build(BuildContext context) => error == null ? ITFeedbackCardView(controller: this) - : CardErrorWidget(error: error); + : CardErrorWidget(error: error!); } class ITFeedbackCardView extends StatelessWidget { diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index aae3b3eeb..cf61c3d49 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -152,7 +152,7 @@ class MessageSpeechToTextCardState extends State { // done fetchig but not results means some kind of error if (speechToTextResponse == null) { return CardErrorWidget( - error: error, + error: error ?? "Failed to fetch speech to text", maxWidth: AppConfig.toolbarMinWidth, ); } diff --git a/lib/pangea/widgets/igc/card_error_widget.dart b/lib/pangea/widgets/igc/card_error_widget.dart index 8c810391b..bbe1bc63b 100644 --- a/lib/pangea/widgets/igc/card_error_widget.dart +++ b/lib/pangea/widgets/igc/card_error_widget.dart @@ -6,14 +6,14 @@ import 'package:fluffychat/pangea/widgets/igc/card_header.dart'; import 'package:flutter/material.dart'; class CardErrorWidget extends StatelessWidget { - final Object? error; + final Object error; final Choreographer? choreographer; final int? offset; final double? maxWidth; const CardErrorWidget({ super.key, - this.error, + required this.error, this.choreographer, this.offset, this.maxWidth, diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index ff494bb38..06e7a1ed7 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -167,7 +167,7 @@ class WordDataCardView extends StatelessWidget { Widget build(BuildContext context) { if (controller.wordNetError != null) { return CardErrorWidget( - error: controller.wordNetError, + error: controller.wordNetError!, maxWidth: AppConfig.toolbarMinWidth, ); } From cd5f2379b8a380512d61be01fccd41ccb6faf700 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 10:46:50 -0400 Subject: [PATCH 080/115] if staging user tries to login to production (or vice versa), log them out --- lib/main.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 6be6edc91..846c34d23 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; @@ -100,6 +101,18 @@ Future startGui(List clients, SharedPreferences store) async { await firstClient?.accountDataLoading; ErrorWidget.builder = (details) => FluffyChatErrorWidget(details); + + // #Pangea + // errors seems to happen a lot when users switch better production / staging + // while testing by accident. If the account is a production account but server is + // staging or vice versa, logout. + final isStagingUser = firstClient?.userID?.domain?.contains("staging"); + final isStagingServer = Environment.isStaging; + if (isStagingServer != isStagingUser) { + await firstClient?.logout(); + } + // Pangea# + runApp(FluffyChatApp(clients: clients, pincode: pin, store: store)); } From 212632a913c82c98339bac5f88bee0e2fb6b75b4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:05:59 -0400 Subject: [PATCH 081/115] if userID is null, don't try to get isRoomAdmin --- .../pangea_room_extension/room_analytics_extension.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 73371b080..b0224f929 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -10,7 +10,7 @@ extension AnalyticsRoomExtension on Room { return; } - if (!isRoomAdmin) return; + if (client.userID == null || !isRoomAdmin) return; final spaceHierarchy = await client.getSpaceHierarchy( id, maxDepth: 1, From dd29817e08fa779c8c026abe08782eadcd2bc413 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 25 Oct 2024 11:28:45 -0400 Subject: [PATCH 082/115] using user languages in tokenization and language detectio --- .../language_detection_controller.dart | 35 +++---- .../controllers/message_data_controller.dart | 41 +++++++- .../pangea_representation_event.dart | 12 ++- lib/pangea/models/token_api_models.dart | 72 ++++++++++++++ lib/pangea/repo/tokens_repo.dart | 95 ------------------- 5 files changed, 130 insertions(+), 125 deletions(-) create mode 100644 lib/pangea/models/token_api_models.dart delete mode 100644 lib/pangea/repo/tokens_repo.dart diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart index bcc9b4140..51d7c973a 100644 --- a/lib/pangea/controllers/language_detection_controller.dart +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -14,26 +14,26 @@ class LanguageDetectionRequest { /// The full text from which to detect the language. String fullText; - /// The base language of the user, if known. Including this is much preferred + /// The base language of the user that sent the meessage, if known. Including this is much preferred /// and should return better results; however, it is not absolutely necessary. /// This property is nullable to allow for situations where the languages are not set /// at the time of the request. - String? userL1; + String? senderL1; - /// The target language of the user. This is expected to be set for the request + /// The target language of the user that sent the message. This is expected to be set for the request /// but is nullable to handle edge cases where it might not be. - String? userL2; + String? senderL2; LanguageDetectionRequest({ required this.fullText, - this.userL1 = "", - required this.userL2, + required this.senderL1, + required this.senderL2, }); Map toJson() => { 'full_text': fullText, - 'user_l1': userL1, - 'user_l2': userL2, + 'sender_l1': senderL1, + 'sender_l2': senderL2, }; @override @@ -41,12 +41,12 @@ class LanguageDetectionRequest { if (identical(this, other)) return true; return other is LanguageDetectionRequest && other.fullText == fullText && - other.userL1 == userL1 && - other.userL2 == userL2; + other.senderL1 == senderL1 && + other.senderL2 == senderL2; } @override - int get hashCode => fullText.hashCode ^ userL1.hashCode ^ userL2.hashCode; + int get hashCode => fullText.hashCode ^ senderL1.hashCode ^ senderL2.hashCode; } class LanguageDetectionResponse { @@ -125,19 +125,6 @@ class LanguageDetectionController { _cacheClearTimer?.cancel(); } - Future detectLanguage( - String fullText, - String? userL2, - String? userL1, - ) async { - final LanguageDetectionRequest params = LanguageDetectionRequest( - fullText: fullText, - userL1: userL1, - userL2: userL2, - ); - return get(params); - } - Future get( LanguageDetectionRequest params, ) async { diff --git a/lib/pangea/controllers/message_data_controller.dart b/lib/pangea/controllers/message_data_controller.dart index a26c558ba..d11c1ea52 100644 --- a/lib/pangea/controllers/message_data_controller.dart +++ b/lib/pangea/controllers/message_data_controller.dart @@ -1,14 +1,19 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; +import 'package:fluffychat/pangea/models/token_api_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/repo/tokens_repo.dart'; +import 'package:fluffychat/pangea/network/requests.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:matrix/matrix.dart'; import '../constants/pangea_event_types.dart'; @@ -49,6 +54,38 @@ class MessageDataController extends BaseController { super.dispose(); } + /// get tokens from the server + static Future _fetchTokens( + String accessToken, + TokensRequestModel request, + ) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.tokenize, + body: request.toJson(), + ); + + final TokensResponseModel response = TokensResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + if (response.tokens.isEmpty) { + ErrorHandler.logError( + e: Exception( + "empty tokens in tokenize response return", + ), + ); + } + + return response; + } + /// get tokens from the server /// if repEventId is not null, send the tokens to the room Future> _getTokens({ @@ -56,7 +93,7 @@ class MessageDataController extends BaseController { required TokensRequestModel req, required Room? room, }) async { - final TokensResponseModel res = await TokensRepo.tokenize( + final TokensResponseModel res = await _fetchTokens( _pangeaController.userController.accessToken, req, ); diff --git a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart index 172f665f4..61d298807 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart @@ -3,8 +3,8 @@ import 'dart:developer'; import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_choreo_event.dart'; 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:fluffychat/pangea/repo/tokens_repo.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -135,13 +135,17 @@ class RepresentationEvent { await MatrixState.pangeaController.messageData.getTokens( repEventId: _event?.eventId, room: _event?.room ?? parentMessageEvent.room, - // Jordan - for just tokens, it's not clear which languages to pass req: TokensRequestModel( fullText: text, - userL1: + langCode: langCode, + senderL1: MatrixState.pangeaController.languageController.userL1?.langCode ?? LanguageKeys.unknownLanguage, - userL2: langCode, + // since langCode is known, senderL2 will be used to determine whether these tokens + // need pos/mporph tags whether lemmas are eligible to marked as "save_vocab=true" + senderL2: + MatrixState.pangeaController.languageController.userL2?.langCode ?? + LanguageKeys.unknownLanguage, ), ); diff --git a/lib/pangea/models/token_api_models.dart b/lib/pangea/models/token_api_models.dart new file mode 100644 index 000000000..fff339ddf --- /dev/null +++ b/lib/pangea/models/token_api_models.dart @@ -0,0 +1,72 @@ +import 'package:fluffychat/pangea/constants/model_keys.dart'; + +import 'pangea_token_model.dart'; + +class TokensRequestModel { + /// the text to be tokenized + String fullText; + + /// if known, [langCode] is the language of of the text + /// it is used to determine which model to use in tokenizing + String? langCode; + + /// [senderL1] and [senderL2] are the languages of the sender + /// if langCode is not known, the [senderL1] and [senderL2] will be used to help determine the language of the text + /// if langCode is known, [senderL1] and [senderL2] will be used to determine whether the tokens need + /// pos/mporph tags and whether lemmas are eligible to marked as "save_vocab=true" + String senderL1; + + /// [senderL1] and [senderL2] are the languages of the sender + /// if langCode is not known, the [senderL1] and [senderL2] will be used to help determine the language of the text + /// if langCode is known, [senderL1] and [senderL2] will be used to determine whether the tokens need + /// pos/mporph tags and whether lemmas are eligible to marked as "save_vocab=true" + String senderL2; + + TokensRequestModel({ + required this.fullText, + required this.langCode, + required this.senderL1, + required this.senderL2, + }); + + Map toJson() => { + ModelKey.fullText: fullText, + ModelKey.userL1: senderL1, + ModelKey.userL2: senderL2, + ModelKey.langCode: langCode, + }; + + // override equals and hashcode + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TokensRequestModel && + other.fullText == fullText && + other.senderL1 == senderL1 && + other.senderL2 == senderL2; + } + + @override + int get hashCode => fullText.hashCode ^ senderL1.hashCode ^ senderL2.hashCode; +} + +class TokensResponseModel { + List tokens; + String lang; + + TokensResponseModel({required this.tokens, required this.lang}); + + factory TokensResponseModel.fromJson( + Map json, + ) => + TokensResponseModel( + tokens: (json[ModelKey.tokens] as Iterable) + .map( + (e) => PangeaToken.fromJson(e as Map), + ) + .toList() + .cast(), + lang: json[ModelKey.lang], + ); +} diff --git a/lib/pangea/repo/tokens_repo.dart b/lib/pangea/repo/tokens_repo.dart deleted file mode 100644 index de539b453..000000000 --- a/lib/pangea/repo/tokens_repo.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart'; - -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import '../config/environment.dart'; -import '../models/pangea_token_model.dart'; -import '../network/requests.dart'; -import '../network/urls.dart'; - -class TokensRepo { - static Future tokenize( - String accessToken, - TokensRequestModel request, - ) async { - final Requests req = Requests( - choreoApiKey: Environment.choreoApiKey, - accessToken: accessToken, - ); - - final Response res = await req.post( - url: PApiUrls.tokenize, - body: request.toJson(), - ); - - final TokensResponseModel response = TokensResponseModel.fromJson( - jsonDecode( - utf8.decode(res.bodyBytes).toString(), - ), - ); - - if (response.tokens.isEmpty) { - ErrorHandler.logError( - e: Exception( - "empty tokens in tokenize response return", - ), - ); - } - - return response; - } -} - -class TokensRequestModel { - String fullText; - String userL1; - String userL2; - - TokensRequestModel({ - required this.fullText, - required this.userL1, - required this.userL2, - }); - - Map toJson() => { - ModelKey.fullText: fullText, - ModelKey.userL1: userL1, - ModelKey.userL2: userL2, - }; - - // override equals and hashcode - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is TokensRequestModel && - other.fullText == fullText && - other.userL1 == userL1 && - other.userL2 == userL2; - } - - @override - int get hashCode => fullText.hashCode ^ userL1.hashCode ^ userL2.hashCode; -} - -class TokensResponseModel { - List tokens; - String lang; - - TokensResponseModel({required this.tokens, required this.lang}); - - factory TokensResponseModel.fromJson( - Map json, - ) => - TokensResponseModel( - tokens: (json[ModelKey.tokens] as Iterable) - .map( - (e) => PangeaToken.fromJson(e as Map), - ) - .toList() - .cast(), - lang: json[ModelKey.lang], - ); -} From df9f8e09161da7093b9aea25f36fd98d11c9a9fc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:37:36 -0400 Subject: [PATCH 083/115] prevent null check error in send button --- lib/pangea/choreographer/controllers/choreographer.dart | 1 + lib/pangea/choreographer/widgets/send_button.dart | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index f3e48a8b6..63895d550 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -593,6 +593,7 @@ class Choreographer { if (isFetching) return false; if (errorService.isError) return true; if (itEnabled && isRunningIT) return false; + if (igc.igcTextData == null) return false; final hasITMatches = igc.igcTextData!.matches.any((match) => match.isITStart); diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index f5e358a31..6fba75395 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -56,7 +56,10 @@ class ChoreographerSendButtonState extends State { color: widget.controller.choreographer.assistanceState .stateColor(context), onPressed: () { - widget.controller.choreographer.send(context); + widget.controller.choreographer.canSendMessage + ? widget.controller.choreographer.send(context) + : widget.controller.choreographer.igc + .showFirstMatch(context); }, tooltip: L10n.of(context)!.send, ), From 693c1b833473dfe6cf5a5749869ff49231123edb Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:38:04 -0400 Subject: [PATCH 084/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e24350240..c4662cdbd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.8+3558 +version: 1.22.9+3559 environment: sdk: ">=3.0.0 <4.0.0" From e110c0a6344b3eb2a06c17e6ca7b9e8e204a4cb1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:42:54 -0400 Subject: [PATCH 085/115] commenting out code to logout on server mixup until tested further --- lib/main.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 846c34d23..05e4ca5cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; @@ -106,11 +105,11 @@ Future startGui(List clients, SharedPreferences store) async { // errors seems to happen a lot when users switch better production / staging // while testing by accident. If the account is a production account but server is // staging or vice versa, logout. - final isStagingUser = firstClient?.userID?.domain?.contains("staging"); - final isStagingServer = Environment.isStaging; - if (isStagingServer != isStagingUser) { - await firstClient?.logout(); - } + // final isStagingUser = firstClient?.userID?.domain?.contains("staging"); + // final isStagingServer = Environment.isStaging; + // if (isStagingServer != isStagingUser) { + // await firstClient?.logout(); + // } // Pangea# runApp(FluffyChatApp(clients: clients, pincode: pin, store: store)); From ee167f01e4bd65627e15e74eaa129ace90399508 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:47:59 -0400 Subject: [PATCH 086/115] fixed error in server mixup test --- lib/main.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 05e4ca5cc..9f5e656bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; @@ -105,13 +106,14 @@ Future startGui(List clients, SharedPreferences store) async { // errors seems to happen a lot when users switch better production / staging // while testing by accident. If the account is a production account but server is // staging or vice versa, logout. - // final isStagingUser = firstClient?.userID?.domain?.contains("staging"); - // final isStagingServer = Environment.isStaging; - // if (isStagingServer != isStagingUser) { - // await firstClient?.logout(); - // } + if (firstClient?.userID?.domain != null) { + final isStagingUser = firstClient!.userID!.domain!.contains("staging"); + final isStagingServer = Environment.isStaging; + if (isStagingServer != isStagingUser) { + await firstClient.logout(); + } + } // Pangea# - runApp(FluffyChatApp(clients: clients, pincode: pin, store: store)); } From 6791e410bd355311c9c81c99daf803fba7fd7edc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 11:56:33 -0400 Subject: [PATCH 087/115] close choreo error button on press --- lib/pangea/choreographer/widgets/has_error_button.dart | 9 +++++---- lib/pangea/widgets/chat/chat_floating_action_button.dart | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pangea/choreographer/widgets/has_error_button.dart b/lib/pangea/choreographer/widgets/has_error_button.dart index 007820139..77a7170d9 100644 --- a/lib/pangea/choreographer/widgets/has_error_button.dart +++ b/lib/pangea/choreographer/widgets/has_error_button.dart @@ -1,15 +1,15 @@ +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:flutter/material.dart'; -import '../../controllers/pangea_controller.dart'; import '../controllers/error_service.dart'; class ChoreographerHasErrorButton extends StatelessWidget { final ChoreoError error; - final PangeaController pangeaController; + final Choreographer choreographer; const ChoreographerHasErrorButton( - this.pangeaController, - this.error, { + this.error, + this.choreographer, { super.key, }); @@ -26,6 +26,7 @@ class ChoreographerHasErrorButton extends StatelessWidget { ), ), ); + choreographer.errorService.resetError(); } }, mini: true, diff --git a/lib/pangea/widgets/chat/chat_floating_action_button.dart b/lib/pangea/widgets/chat/chat_floating_action_button.dart index 35ea1c3eb..ce4128700 100644 --- a/lib/pangea/widgets/chat/chat_floating_action_button.dart +++ b/lib/pangea/widgets/chat/chat_floating_action_button.dart @@ -76,8 +76,8 @@ class ChatFloatingActionButtonState extends State { } if (widget.controller.choreographer.errorService.error != null) { return ChoreographerHasErrorButton( - widget.controller.pangeaController, widget.controller.choreographer.errorService.error!, + widget.controller.choreographer, ); } From 99d25932c00e2429f03b520c772447efd545d22e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 25 Oct 2024 12:15:21 -0400 Subject: [PATCH 088/115] check if overlay renderbox has size before getting size/offset --- lib/pangea/widgets/chat/message_selection_overlay.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 299029a10..4c5cf86da 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -395,6 +395,10 @@ class MessageOverlayController extends State } Size? get messageSize { + if (messageRenderBox == null || !messageRenderBox!.hasSize) { + return null; + } + try { return messageRenderBox?.size; } catch (e, s) { @@ -404,6 +408,10 @@ class MessageOverlayController extends State } Offset? get messageOffset { + if (messageRenderBox == null || !messageRenderBox!.hasSize) { + return null; + } + try { return messageRenderBox?.localToGlobal(Offset.zero); } catch (e, s) { From 73026c39b367c3a775b0c439c7570da64eb742b5 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Fri, 25 Oct 2024 23:32:11 +0700 Subject: [PATCH 089/115] create bot options before invite --- .../widgets/conversation_bot/conversation_bot_settings.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 833ba8924..044fcc214 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -241,6 +241,8 @@ class ConversationBotSettingsDialogState updateFromTextControllers(); + Navigator.of(context).pop(botOptions); + final bool isBotRoomMember = await widget.room.botIsInRoom; if (addBot && !isBotRoomMember) { @@ -248,8 +250,6 @@ class ConversationBotSettingsDialogState } else if (!addBot && isBotRoomMember) { await widget.room.kick(BotName.byEnvironment); } - - Navigator.of(context).pop(botOptions); }, child: Text(L10n.of(context)!.confirm), ), From a3c5ab15d8f21e727b25eb44642bfa18164f87fa Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Mon, 28 Oct 2024 14:53:10 -0400 Subject: [PATCH 090/115] allow sending if in manual igc mode --- .../controllers/choreographer.dart | 18 +++++++++++++++--- lib/pangea/widgets/chat/message_toolbar.dart | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 63895d550..6f65ea836 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -590,19 +590,31 @@ class Choreographer { } bool get canSendMessage { - if (isFetching) return false; + // if there's an error, let them send. we don't want to block them from sending in this case if (errorService.isError) return true; + + // if they're in IT mode, don't let them send if (itEnabled && isRunningIT) return false; - if (igc.igcTextData == null) return false; + // if they've turned off IGC then let them send the message when they want + if (!isAutoIGCEnabled) return true; + + // if we're in the middle of fetching results, don't let them send + 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 they have relevant matches, don't let them send final hasITMatches = igc.igcTextData!.matches.any((match) => match.isITStart); final hasIGCMatches = igc.igcTextData!.matches.any((match) => !match.isITStart); - if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) { return false; } + + // otherwise, let them send return true; } } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 36b3cc45c..bc6ed54bc 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -47,7 +47,7 @@ class MessageToolbar extends StatelessWidget { final bool messageInUserL2 = pangeaMessageEvent.messageDisplayLangCode == MatrixState.pangeaController.languageController.userL2?.langCode; - // If not in the target language, set to nullMode + // If not in the target language show specific messsage if (!messageInUserL2) { return MessageDisplayCard( displayText: From 811c9811488531017c7a77ae5ac3cd8446a9957c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 28 Oct 2024 15:55:56 -0400 Subject: [PATCH 091/115] don't call messagesSinceUpdate from clearMessagesSinceUpdate repeatedly --- lib/pangea/controllers/get_analytics_controller.dart | 3 ++- lib/pangea/controllers/my_analytics_controller.dart | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 66cf39501..26aa7234b 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -162,7 +162,8 @@ class GetAnalyticsController { return formattedCache; } catch (err) { // if something goes wrong while trying to format the local data, clear it - _pangeaController.myAnalytics.clearMessagesSinceUpdate(); + _pangeaController.myAnalytics + .clearMessagesSinceUpdate(clearDrafts: true); return {}; } } catch (exception, stackTrace) { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 6ffa1ef31..48dc67573 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -243,7 +243,12 @@ class MyAnalyticsController extends BaseController { } /// Clears the local cache of recently sent constructs. Called before updating analytics - void clearMessagesSinceUpdate() { + void clearMessagesSinceUpdate({clearDrafts = false}) { + if (clearDrafts) { + _pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate); + return; + } + final localCache = _pangeaController.analytics.messagesSinceUpdate; final draftKeys = localCache.keys.where((key) => key.startsWith('draft')); if (draftKeys.isEmpty) { From 4c4ebcb272160843f540977006d0195d03414719 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 28 Oct 2024 16:04:16 -0400 Subject: [PATCH 092/115] don't call fetchOwnProfile is userID is null --- .../chat_list/client_chooser_button.dart | 119 ++++++++++-------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 34fda564b..0ac5ff0a8 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -214,62 +214,71 @@ class ClientChooserButton extends StatelessWidget { var clientCount = 0; matrix.accountBundles.forEach((key, value) => clientCount += value.length); - return FutureBuilder( - future: matrix.client.fetchOwnProfile(), - builder: (context, snapshot) => Stack( - alignment: Alignment.center, - children: [ - // #Pangea - // ...List.generate( - // clientCount, - // (index) => KeyBoardShortcuts( - // keysToPress: _buildKeyboardShortcut(index + 1), - // helpLabel: L10n.of(context)!.switchToAccount(index + 1), - // onKeysPressed: () => _handleKeyboardShortcut( - // matrix, - // index, - // context, - // ), - // child: const SizedBox.shrink(), - // ), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.nextAccount, - // onKeysPressed: () => _nextAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.controlLeft, - // LogicalKeyboardKey.shiftLeft, - // LogicalKeyboardKey.tab, - // }, - // helpLabel: L10n.of(context)!.previousAccount, - // onKeysPressed: () => _previousAccount(matrix, context), - // child: const SizedBox.shrink(), - // ), - // Pangea# - PopupMenuButton( - onSelected: (o) => _clientSelected(o, context), - itemBuilder: _bundleMenuItems, - 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, - ), + // #Pangea + return matrix.client.userID == null + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), + ) + : + // Pangea# + FutureBuilder( + future: matrix.client.fetchOwnProfile(), + builder: (context, snapshot) => Stack( + alignment: Alignment.center, + children: [ + // #Pangea + // ...List.generate( + // clientCount, + // (index) => KeyBoardShortcuts( + // keysToPress: _buildKeyboardShortcut(index + 1), + // helpLabel: L10n.of(context)!.switchToAccount(index + 1), + // onKeysPressed: () => _handleKeyboardShortcut( + // matrix, + // index, + // context, + // ), + // child: const SizedBox.shrink(), + // ), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.nextAccount, + // onKeysPressed: () => _nextAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.shiftLeft, + // LogicalKeyboardKey.tab, + // }, + // helpLabel: L10n.of(context)!.previousAccount, + // onKeysPressed: () => _previousAccount(matrix, context), + // child: const SizedBox.shrink(), + // ), + // Pangea# + PopupMenuButton( + onSelected: (o) => _clientSelected(o, context), + itemBuilder: _bundleMenuItems, + 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, + ), + ), + ), + ], ), - ), - ], - ), - ); + ); } Set? _buildKeyboardShortcut(int index) { From 2a4f5e3462074e7a49bc07e5bc39d024ed14d76c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 28 Oct 2024 16:07:15 -0400 Subject: [PATCH 093/115] don't try to set read marker if userID is null --- lib/pages/chat/chat.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ebfd384bc..db5041623 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -486,10 +486,11 @@ class ChatController extends State void setReadMarker({String? eventId}) { // #Pangea - if (eventId != null && - (eventId.contains("web") || - eventId.contains("android") || - eventId.contains("ios"))) { + if (room.client.userID == null || + eventId != null && + (eventId.contains("web") || + eventId.contains("android") || + eventId.contains("ios"))) { return; } // Pangea# From 70fd9e9f2e7afbfbcef6e429aba28a407c560036 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 28 Oct 2024 16:21:25 -0400 Subject: [PATCH 094/115] check if mounted before setState in message.dart --- lib/pages/chat/events/message.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index a5e60687d..ba8e35ae2 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -186,7 +186,11 @@ class Message extends StatelessWidget { if (animateIn && resetAnimateIn != null) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { animateIn = false; - setState(resetAnimateIn); + // #Pangea + if (context.mounted) { + // Pangea# + setState(resetAnimateIn); + } }); } return AnimatedSize( From a76eb8cbe9052339a1d2441ad03cca222e50e1d1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 28 Oct 2024 16:31:09 -0400 Subject: [PATCH 095/115] better handling for localizations in instruction popups --- lib/pangea/enum/instructions_enum.dart | 57 +++++++------------------ lib/pangea/utils/inline_tooltip.dart | 5 ++- lib/pangea/utils/instructions.dart | 59 ++++++++++++++------------ 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart index e1a403526..a42a01643 100644 --- a/lib/pangea/enum/instructions_enum.dart +++ b/lib/pangea/enum/instructions_enum.dart @@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/platform_infos.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'; enum InstructionsEnum { @@ -19,24 +18,16 @@ enum InstructionsEnum { } extension InstructionsEnumExtension on InstructionsEnum { - String title(BuildContext context) { - if (!context.mounted) { - ErrorHandler.logError( - e: Exception("Context not mounted"), - m: 'InstructionsEnumExtension.title for $this', - ); - debugger(when: kDebugMode); - return ''; - } + String title(L10n l10n) { switch (this) { case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsTitle; + return l10n.itInstructionsTitle; case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageTitle; + return l10n.clickMessageTitle; case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateTitle; + return l10n.blurMeansTranslateTitle; case InstructionsEnum.tooltipInstructions: - return L10n.of(context)!.tooltipInstructionsTitle; + return l10n.tooltipInstructionsTitle; case InstructionsEnum.clickAgainToDeselect: case InstructionsEnum.speechToText: case InstructionsEnum.l1Translation: @@ -53,46 +44,30 @@ extension InstructionsEnumExtension on InstructionsEnum { } } - String body(BuildContext context) { - if (!context.mounted) { - ErrorHandler.logError( - e: Exception("Context not mounted"), - m: 'InstructionsEnumExtension.body for $this', - ); - debugger(when: kDebugMode); - return ""; - } + String body(L10n l10n) { switch (this) { case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsBody; + return l10n.itInstructionsBody; case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageBody; + return l10n.clickMessageBody; case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateBody; + return l10n.blurMeansTranslateBody; case InstructionsEnum.speechToText: - return L10n.of(context)!.speechToTextBody; + return l10n.speechToTextBody; case InstructionsEnum.l1Translation: - return L10n.of(context)!.l1TranslationBody; + return l10n.l1TranslationBody; case InstructionsEnum.translationChoices: - return L10n.of(context)!.translationChoicesBody; + return l10n.translationChoicesBody; case InstructionsEnum.clickAgainToDeselect: - return L10n.of(context)!.clickTheWordAgainToDeselect; + return l10n.clickTheWordAgainToDeselect; case InstructionsEnum.tooltipInstructions: return PlatformInfos.isMobile - ? L10n.of(context)!.tooltipInstructionsMobileBody - : L10n.of(context)!.tooltipInstructionsBrowserBody; + ? l10n.tooltipInstructionsMobileBody + : l10n.tooltipInstructionsBrowserBody; } } - bool toggledOff(BuildContext context) { - if (!context.mounted) { - ErrorHandler.logError( - e: Exception("Context not mounted"), - m: 'InstructionsEnumExtension.toggledOff for $this', - ); - debugger(when: kDebugMode); - return false; - } + bool toggledOff() { final instructionSettings = MatrixState.pangeaController.userController.profile.instructionSettings; switch (this) { diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart index 26ec05426..4c81e1bbd 100644 --- a/lib/pangea/utils/inline_tooltip.dart +++ b/lib/pangea/utils/inline_tooltip.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class InlineTooltip extends StatelessWidget { final InstructionsEnum instructionsEnum; @@ -15,7 +16,7 @@ class InlineTooltip extends StatelessWidget { @override Widget build(BuildContext context) { - if (instructionsEnum.toggledOff(context)) { + if (instructionsEnum.toggledOff()) { return const SizedBox(); } @@ -42,7 +43,7 @@ class InlineTooltip extends StatelessWidget { // Text in the middle Center( child: Text( - instructionsEnum.body(context), + instructionsEnum.body(L10n.of(context)!), style: TextStyle( color: Theme.of(context).colorScheme.onSurface, height: 1.5, diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 31b552384..681c0de08 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -80,7 +80,7 @@ class InstructionsController { } _instructionsShown[key.toString()] = true; - if (key.toggledOff(context)) { + if (key.toggledOff()) { return; } if (L10n.of(context) == null) { @@ -94,33 +94,36 @@ class InstructionsController { final botStyle = BotStyle.text(context); Future.delayed( const Duration(seconds: 1), - () => OverlayUtil.showPositionedCard( - context: context, - backDropToDismiss: false, - cardToShow: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CardHeader( - text: key.title(context), - botExpression: BotExpression.idle, - onClose: () => {_instructionsClosed[key.toString()] = true}, - ), - const SizedBox(height: 10.0), - Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - key.body(context), - style: botStyle, + () { + if (!context.mounted) return; + OverlayUtil.showPositionedCard( + context: context, + backDropToDismiss: false, + cardToShow: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardHeader( + text: key.title(L10n.of(context)!), + botExpression: BotExpression.idle, + onClose: () => {_instructionsClosed[key.toString()] = true}, ), - ), - if (showToggle) InstructionsToggle(instructionsKey: key), - ], - ), - maxHeight: 300, - maxWidth: 300, - transformTargetId: transformTargetKey, - closePrevOverlay: false, - ), + const SizedBox(height: 10.0), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + key.body(L10n.of(context)!), + style: botStyle, + ), + ), + if (showToggle) InstructionsToggle(instructionsKey: key), + ], + ), + maxHeight: 300, + maxWidth: 300, + transformTargetId: transformTargetKey, + closePrevOverlay: false, + ); + }, ); } } @@ -152,7 +155,7 @@ class InstructionsToggleState extends State { return SwitchListTile.adaptive( activeColor: AppConfig.activeToggleColor, title: Text(L10n.of(context)!.doNotShowAgain), - value: widget.instructionsKey.toggledOff(context), + value: widget.instructionsKey.toggledOff(), onChanged: ((value) async { pangeaController.instructions.setToggledOff( widget.instructionsKey, From 9dbac76a2b08b4538e77269a325f33ec9d47c119 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 10:03:11 -0400 Subject: [PATCH 096/115] update matrix dart sdk version and filter out old analytics data type from sync --- lib/utils/client_manager.dart | 1 + pubspec.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 5ea52f5e5..808dfd9fc 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -139,6 +139,7 @@ abstract class ClientManager { timeline: StateFilter( notTypes: [ PangeaEventTypes.construct, + PangeaEventTypes.summaryAnalytics, ], ), ), diff --git a/pubspec.lock b/pubspec.lock index 99543db43..e869b8a0d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1481,7 +1481,7 @@ packages: description: path: "." ref: main - resolved-ref: "3b77012ba93d6ff8f98dfdff00663aabeddf077e" + resolved-ref: bc99ebf09fa8255a4f6c41bd2d1287b1a5ac4639 url: "https://github.com/pangeachat/matrix-dart-sdk.git" source: git version: "0.32.4" From 7a4e321d14ffc84dd03cf12be37ce47f159bf058 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 10:04:07 -0400 Subject: [PATCH 097/115] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c4662cdbd..c9752349c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.22.9+3559 +version: 1.23.1+3560 environment: sdk: ">=3.0.0 <4.0.0" From b843f85f1cf8d270b2b85abb56f7f4c55a1acdfb Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 10:51:51 -0400 Subject: [PATCH 098/115] update sdk version --- pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e869b8a0d..62b8d21a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1481,7 +1481,7 @@ packages: description: path: "." ref: main - resolved-ref: bc99ebf09fa8255a4f6c41bd2d1287b1a5ac4639 + resolved-ref: "59895c358743fa23d8b52a61a8ec9755139951f8" url: "https://github.com/pangeachat/matrix-dart-sdk.git" source: git version: "0.32.4" diff --git a/pubspec.yaml b/pubspec.yaml index c9752349c..788d4d1ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.23.1+3560 +version: 1.23.2+3561 environment: sdk: ">=3.0.0 <4.0.0" From a0bced5ae704b98ca00ce9399d827a08b34f9895 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 11:22:14 -0400 Subject: [PATCH 099/115] explicitly set enableEncryption to false when creating a new group chat --- lib/pages/chat_list/space_view.dart | 3 +++ lib/pages/new_group/new_group.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index f0f2eda05..41bce1e9a 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -404,6 +404,9 @@ class _SpaceViewState extends State { ), ] : null, + // #Pangea + enableEncryption: false, + // Pangea# ); } await activeSpace.setSpaceChild(roomId); diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index df5f14aa0..f5156d9d6 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -74,6 +74,9 @@ class NewGroupController extends State { content: {'url': avatarUrl.toString()}, ), ], + // #Pangea + enableEncryption: false, + // Pangea# ); if (!mounted) return; context.go('/rooms/$roomId/invite'); From d0caf01e4ddf93c56fc36777147747e173e58a36 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 15:20:55 -0400 Subject: [PATCH 100/115] some refactoring to subscriptions, added auto 1-day pretrial --- .../settings_security/settings_security.dart | 7 +- .../controllers/subscription_controller.dart | 172 +++++++++--------- lib/pangea/controllers/user_controller.dart | 4 +- lib/pangea/models/base_subscription_info.dart | 132 +++++++------- lib/pangea/models/mobile_subscriptions.dart | 166 +++-------------- lib/pangea/models/web_subscriptions.dart | 62 ++----- lib/pangea/pages/p_user_age/p_user_age.dart | 1 + .../settings_subscription.dart | 31 ++-- .../settings_subscription_view.dart | 23 +-- lib/pangea/repo/subscription_repo.dart | 14 +- lib/pangea/utils/subscription_app_id.dart | 7 +- .../chat/message_unsubscribed_card.dart | 2 +- lib/pangea/widgets/igc/paywall_card.dart | 2 +- .../subscription/subscription_buttons.dart | 10 +- .../subscription/subscription_options.dart | 8 +- 15 files changed, 252 insertions(+), 389 deletions(-) diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 859e33e89..f065e833b 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -54,8 +54,9 @@ class SettingsSecurityController extends State { // #Pangea final subscriptionController = MatrixState.pangeaController.subscriptionController; - if (subscriptionController.subscription?.isPaidSubscription == true && - subscriptionController.subscription?.defaultManagementURL != null) { + if (subscriptionController.currentSubscriptionInfo?.isPaidSubscription == + true && + subscriptionController.defaultManagementURL != null) { final resp = await showOkCancelAlertDialog( useRootNavigator: false, context: context, @@ -66,7 +67,7 @@ class SettingsSecurityController extends State { ); if (resp == OkCancelResult.ok) { launchUrlString( - subscriptionController.subscription!.defaultManagementURL!, + subscriptionController.defaultManagementURL!, mode: LaunchMode.externalApplication, ); return; diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart index 94c396cab..61cfc2f88 100644 --- a/lib/pangea/controllers/subscription_controller.dart +++ b/lib/pangea/controllers/subscription_controller.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:convert'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; import 'package:fluffychat/pangea/models/mobile_subscriptions.dart'; import 'package:fluffychat/pangea/models/web_subscriptions.dart'; @@ -13,6 +14,7 @@ import 'package:fluffychat/pangea/network/requests.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; import 'package:fluffychat/pangea/widgets/subscription/subscription_paywall.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; @@ -31,7 +33,10 @@ enum SubscriptionStatus { class SubscriptionController extends BaseController { late PangeaController _pangeaController; - SubscriptionInfo? subscription; + + CurrentSubscriptionInfo? currentSubscriptionInfo; + AvailableSubscriptionsInfo? availableSubscriptionInfo; + final StreamController subscriptionStream = StreamController.broadcast(); final StreamController trialActivationStream = StreamController.broadcast(); @@ -39,10 +44,11 @@ class SubscriptionController extends BaseController { _pangeaController = pangeaController; } + UserController get userController => _pangeaController.userController; + String? get userID => _pangeaController.matrixState.client.userID; + bool get isSubscribed => - subscription != null && - (subscription!.currentSubscriptionId != null || - subscription!.currentSubscription != null); + currentSubscriptionInfo?.currentSubscriptionId != null; bool _isInitializing = false; Completer initialized = Completer(); @@ -67,18 +73,27 @@ class SubscriptionController extends BaseController { Future _initialize() async { try { - if (_pangeaController.matrixState.client.userID == null) { + if (userID == null) { debugPrint( "Attempted to initalize subscription information with null userId", ); return; } - subscription = kIsWeb - ? WebSubscriptionInfo(pangeaController: _pangeaController) - : MobileSubscriptionInfo(pangeaController: _pangeaController); + availableSubscriptionInfo = AvailableSubscriptionsInfo(); + await availableSubscriptionInfo!.setAvailableSubscriptions(); - await subscription!.configure(); + currentSubscriptionInfo = kIsWeb + ? WebSubscriptionInfo( + userID: userID!, + availableSubscriptionInfo: availableSubscriptionInfo!, + ) + : MobileSubscriptionInfo( + userID: userID!, + availableSubscriptionInfo: availableSubscriptionInfo!, + ); + + await currentSubscriptionInfo!.configure(); if (_activatedNewUserTrial) { setNewUserTrial(); } @@ -101,7 +116,7 @@ class SubscriptionController extends BaseController { await _pangeaController.pStoreService.delete( PLocalKey.beganWebPayment, ); - if (_pangeaController.subscriptionController.isSubscribed) { + if (isSubscribed) { subscriptionStream.add(true); } } @@ -170,7 +185,7 @@ class SubscriptionController extends BaseController { return; } ErrorHandler.logError( - m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID} with error code $errCode", + m: "Failed to purchase revenuecat package for user $userID with error code $errCode", s: StackTrace.current, ); return; @@ -178,14 +193,19 @@ class SubscriptionController extends BaseController { } } - bool get _activatedNewUserTrial { - final bool activated = _pangeaController - .userController.profile.userSettings.activatedFreeTrial; - return _pangeaController.userController.inTrialWindow && activated; - } + int get currentTrialDays => userController.inTrialWindow(trialDays: 1) + ? 1 + : userController.inTrialWindow(trialDays: 7) + ? 7 + : 0; + + bool get _activatedNewUserTrial => + userController.inTrialWindow(trialDays: 1) || + (userController.inTrialWindow() && + userController.profile.userSettings.activatedFreeTrial); void activateNewUserTrial() { - _pangeaController.userController.updateProfile( + userController.updateProfile( (profile) { profile.userSettings.activatedFreeTrial = true; return profile; @@ -196,8 +216,7 @@ class SubscriptionController extends BaseController { } void setNewUserTrial() { - final DateTime? createdAt = - _pangeaController.userController.profile.userSettings.createdAt; + final DateTime? createdAt = userController.profile.userSettings.createdAt; if (createdAt == null) { ErrorHandler.logError( m: "Null user profile createAt in subscription settings", @@ -207,23 +226,16 @@ class SubscriptionController extends BaseController { } final DateTime expirationDate = createdAt.add( - const Duration(days: 7), + Duration(days: currentTrialDays), ); - subscription?.setTrial(expirationDate); + currentSubscriptionInfo?.setTrial(expirationDate); } Future updateCustomerInfo() async { if (!initialized.isCompleted) { await initialize(); } - if (subscription == null) { - ErrorHandler.logError( - m: "Null subscription info in subscription settings", - s: StackTrace.current, - ); - return; - } - await subscription!.setCustomerInfo(); + await currentSubscriptionInfo!.setCurrentSubscription(); setState(null); } @@ -284,7 +296,7 @@ class SubscriptionController extends BaseController { if (!initialized.isCompleted) { await initialize(); } - if (subscription?.availableSubscriptions.isEmpty ?? true) { + if (availableSubscriptionInfo?.availableSubscriptions.isEmpty ?? true) { return; } if (isSubscribed) return; @@ -310,70 +322,51 @@ class SubscriptionController extends BaseController { } } - Future getPaymentLink(String duration, {bool isPromo = false}) async { + Future getPaymentLink( + SubscriptionDuration duration, { + bool isPromo = false, + }) async { final Requests req = Requests(baseUrl: PApiUrls.baseAPI); final String reqUrl = Uri.encodeFull( - "${PApiUrls.paymentLink}?pangea_user_id=${_pangeaController.matrixState.client.userID}&duration=$duration&redeem=$isPromo", + "${PApiUrls.paymentLink}?pangea_user_id=$userID&duration=${duration.value}&redeem=$isPromo", ); final Response res = await req.get(url: reqUrl); final json = jsonDecode(res.body); String paymentLink = json["link"]["url"]; - final String? email = await _pangeaController.userController.userEmail; + final String? email = await userController.userEmail; if (email != null) { paymentLink += "?prefilled_email=${Uri.encodeComponent(email)}"; } return paymentLink; } - Future fetchSubscriptionStatus() async { - final Requests req = Requests(baseUrl: PApiUrls.baseAPI); - final String reqUrl = Uri.encodeFull( - "${PApiUrls.subscriptionExpiration}?pangea_user_id=${_pangeaController.matrixState.client.userID}", - ); + String? get defaultManagementURL => + currentSubscriptionInfo?.currentSubscription + ?.defaultManagementURL(availableSubscriptionInfo?.appIds); +} - DateTime? expiration; - try { - final Response res = await req.get(url: reqUrl); - final json = jsonDecode(res.body); - if (json["premium_expires_date"] != null) { - expiration = DateTime.parse(json["premium_expires_date"]); - } - } catch (err) { - ErrorHandler.logError( - e: "Failed to fetch subscripton status for user ${_pangeaController.matrixState.client.userID}", - s: StackTrace.current, - ); - } - final bool subscribed = - expiration == null ? false : DateTime.now().isBefore(expiration); - GoogleAnalytics.updateUserSubscriptionStatus(subscribed); - return subscribed; - } +enum SubscriptionPeriodType { + normal, + trial, +} - Future redeemPromoCode(BuildContext context) async { - final List? promoCode = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.enterPromoCode, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [const DialogTextField()], - ); - if (promoCode == null || promoCode.single.isEmpty) return; - launchUrlString( - "${AppConfig.iosPromoCode}${promoCode.single}", - ); - } +enum SubscriptionDuration { + month, + year, +} + +extension SubscriptionDurationExtension on SubscriptionDuration { + String get value => this == SubscriptionDuration.month ? "month" : "year"; } class SubscriptionDetails { - double price; - String? duration; - Package? package; - String? appId; + final double price; + final SubscriptionDuration? duration; + final String? appId; final String id; - String? periodType = "normal"; + SubscriptionPeriodType periodType; + Package? package; SubscriptionDetails({ required this.price, @@ -381,30 +374,35 @@ class SubscriptionDetails { this.duration, this.package, this.appId, - this.periodType, + this.periodType = SubscriptionPeriodType.normal, }); - void makeTrial() => periodType = 'trial'; - bool get isTrial => periodType == 'trial'; + void makeTrial() => periodType = SubscriptionPeriodType.trial; + bool get isTrial => periodType == SubscriptionPeriodType.trial; - String displayPrice(BuildContext context) { - if (isTrial || price <= 0) { - return L10n.of(context)!.freeTrial; - } - return "\$${price.toStringAsFixed(2)}"; - } + String displayPrice(BuildContext context) => isTrial || price <= 0 + ? L10n.of(context)!.freeTrial + : "\$${price.toStringAsFixed(2)}"; String displayName(BuildContext context) { if (isTrial) { return L10n.of(context)!.oneWeekTrial; } switch (duration) { - case ('month'): + case (SubscriptionDuration.month): return L10n.of(context)!.monthlySubscription; - case ('year'): + case (SubscriptionDuration.year): return L10n.of(context)!.yearlySubscription; default: return L10n.of(context)!.defaultSubscription; } } + + String? defaultManagementURL(SubscriptionAppIds? appIds) { + return appId == appIds?.androidId + ? AppConfig.googlePlayMangementUrl + : appId == appIds?.appleId + ? AppConfig.appleMangementUrl + : Environment.stripeManagementUrl; + } } diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index ca7e2869a..381c08c4d 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -196,13 +196,13 @@ class UserController extends BaseController { } /// Returns a boolean value indicating whether the user is currently in the trial window. - bool get inTrialWindow { + bool inTrialWindow({int trialDays = 7}) { final DateTime? createdAt = profile.userSettings.createdAt; if (createdAt == null) { return false; } return createdAt.isAfter( - DateTime.now().subtract(const Duration(days: 7)), + DateTime.now().subtract(Duration(days: trialDays)), ); } diff --git a/lib/pangea/models/base_subscription_info.dart b/lib/pangea/models/base_subscription_info.dart index 469ac5786..eda6d8a4b 100644 --- a/lib/pangea/models/base_subscription_info.dart +++ b/lib/pangea/models/base_subscription_info.dart @@ -1,50 +1,32 @@ +import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/config/environment.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/repo/subscription_repo.dart'; import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; -class SubscriptionInfo { - PangeaController pangeaController; - List availableSubscriptions = []; - String? currentSubscriptionId; - SubscriptionDetails? currentSubscription; - // Gabby - is it necessary to store appIds for each platform? - SubscriptionAppIds? appIds; - List? allProducts; - final SubscriptionPlatform platform = SubscriptionPlatform(); - List allEntitlements = []; - DateTime? expirationDate; +/// Contains information about the users's current subscription +class CurrentSubscriptionInfo { + final String userID; + final AvailableSubscriptionsInfo availableSubscriptionInfo; - bool get hasSubscribed => allEntitlements.isNotEmpty; - - SubscriptionInfo({ - required this.pangeaController, - }) : super(); - - Future configure() async {} + DateTime? expirationDate; + String? currentSubscriptionId; - //TO-DO - hey Gabby this file feels like it could be reorganized. i'd like to - // 1) move these api calls to a class in a file in repo and - // 2) move the url to the urls file. - // 3) any stateful info to the subscription controller - // let's discuss before you make the changes though - // maybe you had some reason for this organization - - /* - Fetch App Ids for each RC app (iOS, Android, and Stripe). Used to determine which app a user - with an active subscription purchased that subscription. - */ - Future setAppIds() async { - if (appIds != null) return; - appIds = await SubscriptionRepo.getAppIds(); + CurrentSubscriptionInfo({ + required this.userID, + required this.availableSubscriptionInfo, + }); + + SubscriptionDetails? get currentSubscription { + if (currentSubscriptionId == null) return null; + return availableSubscriptionInfo.allProducts?.firstWhereOrNull( + (SubscriptionDetails sub) => + sub.id.contains(currentSubscriptionId!) || + currentSubscriptionId!.contains(sub.id), + ); } - Future setAllProducts() async { - if (allProducts != null) return; - allProducts = await SubscriptionRepo.getAllProducts(); - } + Future configure() async {} bool get isNewUserTrial => currentSubscriptionId == AppConfig.trialSubscriptionId; @@ -64,41 +46,69 @@ class SubscriptionInfo { String? get purchasePlatformDisplayName { if (currentSubscription?.appId == null) return null; - return appIds?.appDisplayName(currentSubscription!.appId!); + return availableSubscriptionInfo.appIds + ?.appDisplayName(currentSubscription!.appId!); } bool get purchasedOnWeb => - (currentSubscription != null && appIds != null) && - (currentSubscription?.appId == appIds?.stripeId); + (currentSubscription != null && + availableSubscriptionInfo.appIds != null) && + (currentSubscription?.appId == + availableSubscriptionInfo.appIds?.stripeId); bool get currentPlatformMatchesPurchasePlatform => - (currentSubscription != null && appIds != null) && - (currentSubscription?.appId == appIds?.currentAppId); + (currentSubscription != null && + availableSubscriptionInfo.appIds != null) && + (currentSubscription?.appId == + availableSubscriptionInfo.appIds?.currentAppId); - void resetSubscription() { - currentSubscription = null; - currentSubscriptionId = null; - } + void resetSubscription() => currentSubscriptionId = null; void setTrial(DateTime expiration) { - if (currentSubscription != null) return; expirationDate = expiration; currentSubscriptionId = AppConfig.trialSubscriptionId; - currentSubscription = SubscriptionDetails( - price: 0, - id: AppConfig.trialSubscriptionId, - periodType: 'trial', - ); + if (currentSubscription == null) { + availableSubscriptionInfo.availableSubscriptions.add( + SubscriptionDetails( + price: 0, + id: AppConfig.trialSubscriptionId, + periodType: SubscriptionPeriodType.trial, + ), + ); + } } - Future setCustomerInfo() async {} + Future setCurrentSubscription() async {} +} + +/// Contains information about the suscriptions available on revenuecat +class AvailableSubscriptionsInfo { + List availableSubscriptions = []; + SubscriptionAppIds? appIds; + List? allProducts; - String? get defaultManagementURL { - final String? purchaseAppId = currentSubscription?.appId; - return purchaseAppId == appIds?.androidId - ? AppConfig.googlePlayMangementUrl - : purchaseAppId == appIds?.appleId - ? AppConfig.appleMangementUrl - : Environment.stripeManagementUrl; + Future setAvailableSubscriptions() async { + appIds ??= await SubscriptionRepo.getAppIds(); + allProducts ??= await SubscriptionRepo.getAllProducts(); + availableSubscriptions = (allProducts ?? []) + .where((product) => product.appId == appIds!.currentAppId) + .sorted((a, b) => a.price.compareTo(b.price)) + .toList(); + // //@Gabby - temporary solution to add trial to list + // if (currentSubscriptionId == null && !hasSubscribed) { + // final id = availableSubscriptions[0].id; + // final package = availableSubscriptions[0].package; + // final duration = availableSubscriptions[0].duration; + // availableSubscriptions.insert( + // 0, + // SubscriptionDetails( + // price: 0, + // id: id, + // duration: duration, + // package: package, + // periodType: SubscriptionPeriodType.trial, + // ), + // ); + // } } } diff --git a/lib/pangea/models/mobile_subscriptions.dart b/lib/pangea/models/mobile_subscriptions.dart index 00fc6e3fd..cf5685118 100644 --- a/lib/pangea/models/mobile_subscriptions.dart +++ b/lib/pangea/models/mobile_subscriptions.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; @@ -9,8 +8,11 @@ import 'package:flutter/material.dart'; import 'package:purchases_flutter/purchases_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class MobileSubscriptionInfo extends SubscriptionInfo { - MobileSubscriptionInfo({required super.pangeaController}) : super(); +class MobileSubscriptionInfo extends CurrentSubscriptionInfo { + MobileSubscriptionInfo({ + required super.userID, + required super.availableSubscriptionInfo, + }); @override Future configure() async { @@ -19,112 +21,42 @@ class MobileSubscriptionInfo extends SubscriptionInfo { : PurchasesConfiguration(Environment.rcIosKey); try { await Purchases.configure( - configuration..appUserID = pangeaController.userController.userId, + configuration..appUserID = userID, ); + await super.configure(); + await setMobilePackages(); } catch (err) { ErrorHandler.logError( - m: "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", + m: "Failed to configure revenuecat SDK", s: StackTrace.current, ); - debugPrint( - "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", - ); - return; - } - await setAppIds(); - await setAllProducts(); - await setCustomerInfo(); - await setMobilePackages(); - if (allProducts != null && appIds != null) { - availableSubscriptions = allProducts! - .where((product) => product.appId == appIds!.currentAppId) - .toList(); - availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); - - if (currentSubscriptionId == null && !hasSubscribed) { - //@Gabby - temporary solution to add trial to list - final id = availableSubscriptions[0].id; - final package = availableSubscriptions[0].package; - final duration = availableSubscriptions[0].duration; - availableSubscriptions.insert( - 0, - SubscriptionDetails( - price: 0, - id: id, - duration: duration, - package: package, - periodType: 'trial', - ), - ); - } - } else { - ErrorHandler.logError(e: Exception("allProducts null || appIds null")); } } Future setMobilePackages() async { - if (allProducts == null) { - ErrorHandler.logError( - m: "Null appProducts in setMobilePrices", - s: StackTrace.current, - ); - debugPrint( - "Null appProducts in setMobilePrices", - ); - return; - } - Offerings offerings; - try { - offerings = await Purchases.getOfferings(); - } catch (err) { - ErrorHandler.logError( - m: "Failed to fetch revenuecat offerings from revenuecat", - s: StackTrace.current, - ); - debugPrint( - "Failed to fetch revenuecat offerings from revenuecat", - ); - return; - } + if (availableSubscriptionInfo.allProducts == null) return; + + final Offerings offerings = await Purchases.getOfferings(); final Offering? offering = offerings.all[Environment.rcOfferingName]; - if (offering != null) { - final List mobileSubscriptions = - offering.availablePackages - .map( - (package) { - return SubscriptionDetails( - price: package.storeProduct.price, - id: package.storeProduct.identifier, - package: package, - ); - }, - ) - .toList() - .cast(); - for (final SubscriptionDetails mobileSub in mobileSubscriptions) { - final int productIndex = allProducts! - .indexWhere((product) => product.id.contains(mobileSub.id)); - if (productIndex >= 0) { - final SubscriptionDetails updated = allProducts![productIndex]; - updated.package = mobileSub.package; - allProducts![productIndex] = updated; - } - } + if (offering == null) return; + + final products = availableSubscriptionInfo.allProducts; + for (final package in offering.availablePackages) { + final int productIndex = products!.indexWhere( + (product) => product.id.contains(package.storeProduct.identifier), + ); + + if (productIndex < 0) continue; + final SubscriptionDetails updated = + availableSubscriptionInfo.allProducts![productIndex]; + updated.package = package; + availableSubscriptionInfo.allProducts![productIndex] = updated; } } @override - Future setCustomerInfo() async { - if (allProducts == null) { - ErrorHandler.logError( - m: "Null allProducts in setCustomerInfo", - s: StackTrace.current, - ); - debugPrint( - "Null allProducts in setCustomerInfo", - ); - return; - } + Future setCurrentSubscription() async { + if (availableSubscriptionInfo.allProducts == null) return; CustomerInfo info; try { @@ -132,28 +64,11 @@ class MobileSubscriptionInfo extends SubscriptionInfo { info = await Purchases.getCustomerInfo(); } catch (err) { ErrorHandler.logError( - m: "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", + m: "Failed to fetch revenuecat customer info", s: StackTrace.current, ); - debugPrint( - "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", - ); return; } - final List noExpirations = - getEntitlementsWithoutExpiration(info); - - if (noExpirations.isNotEmpty) { - Sentry.addBreadcrumb( - Breadcrumb( - message: - "Found revenuecat entitlement(s) without expiration date for user ${pangeaController.userController.userId}: ${noExpirations.map( - (entry) => - "Entitlement Id: ${entry.identifier}, Purchase Date: ${entry.originalPurchaseDate}", - )}", - ), - ); - } final List activeEntitlements = info.entitlements.all.entries @@ -166,14 +81,6 @@ class MobileSubscriptionInfo extends SubscriptionInfo { .map((MapEntry entry) => entry.value) .toList(); - allEntitlements = info.entitlements.all.entries - .map( - (MapEntry entry) => - entry.value.productIdentifier, - ) - .cast() - .toList(); - if (activeEntitlements.length > 1) { debugPrint( "User has more than one active entitlement.", @@ -185,13 +92,9 @@ class MobileSubscriptionInfo extends SubscriptionInfo { } return; } + final EntitlementInfo activeEntitlement = activeEntitlements[0]; currentSubscriptionId = activeEntitlement.productIdentifier; - currentSubscription = allProducts!.firstWhereOrNull( - (SubscriptionDetails sub) => - sub.id.contains(currentSubscriptionId!) || - currentSubscriptionId!.contains(sub.id), - ); expirationDate = activeEntitlement.expirationDate != null ? DateTime.parse(activeEntitlement.expirationDate!) : null; @@ -205,15 +108,4 @@ class MobileSubscriptionInfo extends SubscriptionInfo { ); } } - - List getEntitlementsWithoutExpiration(CustomerInfo info) { - final List noExpirations = info.entitlements.all.entries - .where( - (MapEntry entry) => - entry.value.expirationDate == null, - ) - .map((MapEntry entry) => entry.value) - .toList(); - return noExpirations; - } } diff --git a/lib/pangea/models/web_subscriptions.dart b/lib/pangea/models/web_subscriptions.dart index 1a6cc722a..0f8362843 100644 --- a/lib/pangea/models/web_subscriptions.dart +++ b/lib/pangea/models/web_subscriptions.dart @@ -1,61 +1,23 @@ -import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; import 'package:fluffychat/pangea/repo/subscription_repo.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class WebSubscriptionInfo extends SubscriptionInfo { - WebSubscriptionInfo({required super.pangeaController}) : super(); +class WebSubscriptionInfo extends CurrentSubscriptionInfo { + WebSubscriptionInfo({ + required super.userID, + required super.availableSubscriptionInfo, + }); @override - Future configure() async { - await setAppIds(); - await setAllProducts(); - await setCustomerInfo(); - - if (allProducts == null || appIds == null) { - Sentry.addBreadcrumb( - Breadcrumb(message: "No products found for current app"), - ); - return; - } - - availableSubscriptions = allProducts! - .where((product) => product.appId == appIds!.currentAppId) - .toList(); - availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); - //@Gabby - temporary solution to add trial to list - if (currentSubscriptionId == null && !hasSubscribed) { - final id = availableSubscriptions[0].id; - final package = availableSubscriptions[0].package; - final duration = availableSubscriptions[0].duration; - availableSubscriptions.insert( - 0, - SubscriptionDetails( - price: 0, - id: id, - duration: duration, - package: package, - periodType: 'trial', - ), - ); - } - } - - @override - Future setCustomerInfo() async { - if (currentSubscriptionId != null && currentSubscription != null) { - return; - } - final RCSubscriptionResponseModel currentSubscriptionInfo = - await SubscriptionRepo.getCurrentSubscriptionInfo( - pangeaController.matrixState.client.userID, - allProducts, + Future setCurrentSubscription() async { + if (currentSubscriptionId != null) return; + final rcResponse = await SubscriptionRepo.getCurrentSubscriptionInfo( + userID, + availableSubscriptionInfo.allProducts, ); - currentSubscriptionId = currentSubscriptionInfo.currentSubscriptionId; - currentSubscription = currentSubscriptionInfo.currentSubscription; - allEntitlements = currentSubscriptionInfo.allEntitlements ?? []; - expirationDate = currentSubscriptionInfo.expirationDate; + currentSubscriptionId = rcResponse.currentSubscriptionId; + expirationDate = rcResponse.expirationDate; if (currentSubscriptionId != null && currentSubscription == null) { Sentry.addBreadcrumb( diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart index 5fe489485..fd937485c 100644 --- a/lib/pangea/pages/p_user_age/p_user_age.dart +++ b/lib/pangea/pages/p_user_age/p_user_age.dart @@ -91,6 +91,7 @@ class PUserAgeController extends State { return profile; }); } + pangeaController.subscriptionController.reinitialize(); FluffyChatApp.router.go('/rooms'); } catch (err, s) { setState(() { diff --git a/lib/pangea/pages/settings_subscription/settings_subscription.dart b/lib/pangea/pages/settings_subscription/settings_subscription.dart index 374abedb3..5e9b6139c 100644 --- a/lib/pangea/pages/settings_subscription/settings_subscription.dart +++ b/lib/pangea/pages/settings_subscription/settings_subscription.dart @@ -57,30 +57,33 @@ class SubscriptionManagementController extends State { } bool get subscriptionsAvailable => - subscriptionController.subscription?.availableSubscriptions.isNotEmpty ?? + subscriptionController + .availableSubscriptionInfo?.availableSubscriptions.isNotEmpty ?? false; bool get currentSubscriptionAvailable => subscriptionController.isSubscribed && - subscriptionController.subscription?.currentSubscription != null; + subscriptionController.currentSubscriptionInfo?.currentSubscription != + null; - String? get purchasePlatformDisplayName => - subscriptionController.subscription?.purchasePlatformDisplayName; + String? get purchasePlatformDisplayName => subscriptionController + .currentSubscriptionInfo?.purchasePlatformDisplayName; bool get currentSubscriptionIsPromotional => - subscriptionController.subscription?.currentSubscriptionIsPromotional ?? + subscriptionController + .currentSubscriptionInfo?.currentSubscriptionIsPromotional ?? false; bool get isNewUserTrial => - subscriptionController.subscription?.isNewUserTrial ?? false; + subscriptionController.currentSubscriptionInfo?.isNewUserTrial ?? false; String get currentSubscriptionTitle => - subscriptionController.subscription?.currentSubscription + subscriptionController.currentSubscriptionInfo?.currentSubscription ?.displayName(context) ?? ""; String get currentSubscriptionPrice => - subscriptionController.subscription?.currentSubscription + subscriptionController.currentSubscriptionInfo?.currentSubscription ?.displayPrice(context) ?? ""; @@ -88,11 +91,11 @@ class SubscriptionManagementController extends State { if (!currentSubscriptionAvailable || isNewUserTrial) { return false; } - if (subscriptionController.subscription!.purchasedOnWeb) { + if (subscriptionController.currentSubscriptionInfo!.purchasedOnWeb) { return true; } return subscriptionController - .subscription!.currentPlatformMatchesPurchasePlatform; + .currentSubscriptionInfo!.currentPlatformMatchesPurchasePlatform; } void submitChange({bool isPromo = false}) { @@ -122,12 +125,12 @@ class SubscriptionManagementController extends State { if (email != null) { managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}"; } - final String? purchaseAppId = - subscriptionController.subscription?.currentSubscription?.appId; + final String? purchaseAppId = subscriptionController + .currentSubscriptionInfo?.currentSubscription?.appId; if (purchaseAppId == null) return; final SubscriptionAppIds? appIds = - subscriptionController.subscription!.appIds; + subscriptionController.availableSubscriptionInfo!.appIds; if (purchaseAppId == appIds?.stripeId) { launchUrlString(managementUrl); @@ -167,7 +170,7 @@ class SubscriptionManagementController extends State { } bool isCurrentSubscription(SubscriptionDetails subscription) => - subscriptionController.subscription?.currentSubscription == + subscriptionController.currentSubscriptionInfo?.currentSubscription == subscription || isNewUserTrial && subscription.isTrial; diff --git a/lib/pangea/pages/settings_subscription/settings_subscription_view.dart b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart index 9d9f21bf7..c7c89e1d2 100644 --- a/lib/pangea/pages/settings_subscription/settings_subscription_view.dart +++ b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart @@ -51,6 +51,8 @@ class SettingsSubscriptionView extends StatelessWidget { ), ]; + final isSubscribed = controller.subscriptionController.isSubscribed; + return Scaffold( appBar: AppBar( centerTitle: true, @@ -63,13 +65,11 @@ class SettingsSubscriptionView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - if (controller.subscriptionController.isSubscribed && - !controller.showManagementOptions) + if (isSubscribed && !controller.showManagementOptions) ManagementNotAvailableWarning( controller: controller, ), - if (!(controller.subscriptionController.isSubscribed) || - controller.isNewUserTrial) + if (!isSubscribed || controller.isNewUserTrial) ChangeSubscription(controller: controller), if (controller.showManagementOptions) ...managementButtons, ], @@ -90,13 +90,14 @@ class ManagementNotAvailableWarning extends StatelessWidget { @override Widget build(BuildContext context) { + final currentSubscriptionInfo = + controller.subscriptionController.currentSubscriptionInfo; + String getWarningText() { final DateFormat formatter = DateFormat('yyyy-MM-dd'); if (controller.isNewUserTrial) { return L10n.of(context)!.trialExpiration( - formatter.format( - controller.subscriptionController.subscription!.expirationDate!, - ), + formatter.format(currentSubscriptionInfo!.expirationDate!), ); } if (controller.currentSubscriptionAvailable) { @@ -108,15 +109,11 @@ class ManagementNotAvailableWarning extends StatelessWidget { return warningText; } if (controller.currentSubscriptionIsPromotional) { - if (controller - .subscriptionController.subscription?.isLifetimeSubscription ?? - false) { + if (currentSubscriptionInfo?.isLifetimeSubscription ?? false) { return L10n.of(context)!.promotionalSubscriptionDesc; } return L10n.of(context)!.promoSubscriptionExpirationDesc( - formatter.format( - controller.subscriptionController.subscription!.expirationDate!, - ), + formatter.format(currentSubscriptionInfo!.expirationDate!), ); } return L10n.of(context)!.subscriptionManagementUnavailable; diff --git a/lib/pangea/repo/subscription_repo.dart b/lib/pangea/repo/subscription_repo.dart index 16ea60c4d..293977921 100644 --- a/lib/pangea/repo/subscription_repo.dart +++ b/lib/pangea/repo/subscription_repo.dart @@ -1,14 +1,13 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:http/http.dart' as http; - import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + import '../network/urls.dart'; class SubscriptionRepo { @@ -120,7 +119,9 @@ class RCProductsResponseModel { .map( (productDetails) => SubscriptionDetails( price: double.parse(metadata['$packageId.price']), - duration: metadata['$packageId.duration'], + duration: SubscriptionDuration.values.firstWhereOrNull( + (duration) => duration.value == metadata['$packageId.duration'], + ), id: productDetails['product']['store_identifier'], appId: productDetails['product']['app_id'], ), @@ -150,9 +151,6 @@ class RCSubscriptionResponseModel { final List activeEntitlements = RCSubscriptionResponseModel.getActiveEntitlements(json); - final List allEntitlements = - RCSubscriptionResponseModel.getAllEntitlements(json); - if (activeEntitlements.length > 1) { debugPrint( "User has more than one active entitlement. This shouldn't happen", diff --git a/lib/pangea/utils/subscription_app_id.dart b/lib/pangea/utils/subscription_app_id.dart index 20a69d4a2..c6de8867c 100644 --- a/lib/pangea/utils/subscription_app_id.dart +++ b/lib/pangea/utils/subscription_app_id.dart @@ -49,15 +49,14 @@ enum RCPlatform { apple, } -class SubscriptionPlatform { - RCPlatform currentPlatform = kIsWeb +extension RCPlatformExtension on RCPlatform { + RCPlatform get currentPlatform => kIsWeb ? RCPlatform.stripe : Platform.isAndroid ? RCPlatform.android : RCPlatform.apple; - @override - String toString() { + String get string { return currentPlatform == RCPlatform.stripe ? 'stripe' : currentPlatform == RCPlatform.android diff --git a/lib/pangea/widgets/chat/message_unsubscribed_card.dart b/lib/pangea/widgets/chat/message_unsubscribed_card.dart index 99a08456e..2e62a558e 100644 --- a/lib/pangea/widgets/chat/message_unsubscribed_card.dart +++ b/lib/pangea/widgets/chat/message_unsubscribed_card.dart @@ -16,7 +16,7 @@ class MessageUnsubscribedCard extends StatelessWidget { @override Widget build(BuildContext context) { final bool inTrialWindow = - MatrixState.pangeaController.userController.inTrialWindow; + MatrixState.pangeaController.userController.inTrialWindow(); return Padding( padding: const EdgeInsets.all(16), diff --git a/lib/pangea/widgets/igc/paywall_card.dart b/lib/pangea/widgets/igc/paywall_card.dart index 20b5fcefe..9308016fb 100644 --- a/lib/pangea/widgets/igc/paywall_card.dart +++ b/lib/pangea/widgets/igc/paywall_card.dart @@ -17,7 +17,7 @@ class PaywallCard extends StatelessWidget { @override Widget build(BuildContext context) { final bool inTrialWindow = - MatrixState.pangeaController.userController.inTrialWindow; + MatrixState.pangeaController.userController.inTrialWindow(); return Column( mainAxisSize: MainAxisSize.max, diff --git a/lib/pangea/widgets/subscription/subscription_buttons.dart b/lib/pangea/widgets/subscription/subscription_buttons.dart index 2af95056a..dd922e216 100644 --- a/lib/pangea/widgets/subscription/subscription_buttons.dart +++ b/lib/pangea/widgets/subscription/subscription_buttons.dart @@ -15,14 +15,16 @@ class SubscriptionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final bool inTrialWindow = pangeaController.userController.inTrialWindow; + final bool inTrialWindow = pangeaController.userController.inTrialWindow(); return ListView.builder( shrinkWrap: true, - itemCount: controller - .subscriptionController.subscription!.availableSubscriptions.length, + itemCount: controller.subscriptionController.availableSubscriptionInfo! + .availableSubscriptions.length, itemBuilder: (BuildContext context, int i) { final SubscriptionDetails subscription = pangeaController - .subscriptionController.subscription!.availableSubscriptions[i]; + .subscriptionController + .availableSubscriptionInfo! + .availableSubscriptions[i]; return Column( children: [ ListTile( diff --git a/lib/pangea/widgets/subscription/subscription_options.dart b/lib/pangea/widgets/subscription/subscription_options.dart index d40f68023..5584d37fd 100644 --- a/lib/pangea/widgets/subscription/subscription_options.dart +++ b/lib/pangea/widgets/subscription/subscription_options.dart @@ -19,7 +19,7 @@ class SubscriptionOptions extends StatelessWidget { alignment: WrapAlignment.center, direction: Axis.horizontal, spacing: 10, - children: pangeaController.userController.inTrialWindow + children: pangeaController.userController.inTrialWindow() ? [ SubscriptionCard( onTap: () => pangeaController.subscriptionController @@ -27,7 +27,7 @@ class SubscriptionOptions extends StatelessWidget { SubscriptionDetails( price: 0, id: "", - periodType: 'trial', + periodType: SubscriptionPeriodType.trial, ), context, ), @@ -36,8 +36,8 @@ class SubscriptionOptions extends StatelessWidget { buttonText: L10n.of(context)!.activateTrial, ), ] - : pangeaController - .subscriptionController.subscription!.availableSubscriptions + : pangeaController.subscriptionController.availableSubscriptionInfo! + .availableSubscriptions .map( (subscription) => SubscriptionCard( subscription: subscription, From 4e6ac3348c9750be4c0e3c9685a20a6500d45553 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 29 Oct 2024 15:52:44 -0400 Subject: [PATCH 101/115] call setCustomerSubscription after configuring subscription info --- lib/pangea/controllers/subscription_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart index 61cfc2f88..cffd64748 100644 --- a/lib/pangea/controllers/subscription_controller.dart +++ b/lib/pangea/controllers/subscription_controller.dart @@ -94,6 +94,7 @@ class SubscriptionController extends BaseController { ); await currentSubscriptionInfo!.configure(); + await currentSubscriptionInfo!.setCurrentSubscription(); if (_activatedNewUserTrial) { setNewUserTrial(); } From b4bfafe11bd5eb9e77aaee250115a00dbf85a44f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 30 Oct 2024 12:21:54 -0400 Subject: [PATCH 102/115] updated some copy --- assets/l10n/intl_en.arb | 51 ++---- assets/l10n/intl_es.arb | 154 +++++++++++++++++- .../class_description_button.dart | 4 +- .../p_class_widgets/room_capacity_button.dart | 38 +++-- 4 files changed, 184 insertions(+), 63 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c54d4e7b0..1cd522a5c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3656,11 +3656,6 @@ "unknownPrivateChat": "Unknown private chat", "copyClassCodeDesc": "Users who are already in the app can 'Join space' via the main menu.", "addToSpaceDesc": "Adding a chat to a space will make the chat appear within the space for students and give them access.", - "@addToSpaceDesc": { - "placeholders": { - "roomtype": {} - } - }, "invitedToSpace": "{user} has invited you to join a space: {space}! Do you wish to accept?", "@invitedToSpace": { "placeholders": { @@ -4008,7 +4003,7 @@ "accuracy": "Accuracy", "points": "Points", "noPaymentInfo": "No payment info necessary!", - "conversationBotModeSelectDescription": "Chat Activity", + "conversationBotModeSelectDescription": "Chat activity", "conversationBotModeSelectOption_discussion": "Discussion", "conversationBotModeSelectOption_custom": "Custom", "conversationBotModeSelectOption_conversation": "Conversation", @@ -4029,7 +4024,7 @@ "conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt", "conversationBotCustomZone_customSystemPromptEmptyError": "Missing custom system prompt", "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction", - "botConfig": "Chat Settings", + "botConfig": "Chat settings", "addConversationBotDialogTitleInvite": "Confirm inviting conversation bot", "addConversationBotButtonInvite": "Invite", "addConversationBotDialogInviteConfirmation": "Invite", @@ -4059,39 +4054,21 @@ "tooltipInstructionsMobileBody": "Press and hold items to view tooltips.", "tooltipInstructionsBrowserBody": "Hover over items to view tooltips.", "addSpaceToSpaceDescription": "Select a space to add as a parent", - "roomCapacity": "{roomType} Capacity", - "@roomCapacity": { - "type": "text", - "placeholders": { - "roomType": {} - } - }, + "chatCapacity": "Chat capacity", + "spaceCapacity": "Space capacity", "roomFull": "This room is already at capacity.", "topicNotSet": "The topic has not been set.", - "capacityNotSet": "This room has no capacity limit.", - "roomCapacityHasBeenChanged": "{roomType} capacity changed", - "@roomCapacityHasBeenChanged": { - "type": "text", - "placeholders": { - "roomType": {} - } - }, - "roomExceedsCapacity": "Room exceeds capacity. Consider removing students from the room, or raising the capacity.", - "capacitySetTooLow": "{roomType} capacity cannot be set below the current number of non-admins.", - "@capacitySetTooLow": { - "type": "text", - "placeholders": { - "roomType": {} - } - }, - "roomCapacityExplanation": "{roomType} capacity limits the number of non-admins allowed in a room.", + "chatCapacityNotSet": "This chat has no capacity limit.", + "spaceCapacityNotSet": "This space has no capacity limit.", + "chatCapacityHasBeenChanged": "Chat capacity changed", + "spaceCapacityHasBeenChanged": "Space capacity changed", + "chatCapacitySetTooLow": "Chat capacity cannot be set below the current number of non-admins.", + "spaceCapacitySetTooLow": "Space capacity cannot be set below the current number of non-admins.", + "chatCapacityExplanation": "Chat capacity limits the number of non-admins allowed in a chat.", + "spaceCapacityExplanation": "Space capacity limits the number of non-admins allowed in a space.", + "chatExceedsCapacity": "This chat exceeds its capacity.", + "spaceExceedsCapacity": "This space exceeds its capacity.", "tooManyRequest": "Too many request, please try again later.", - "@roomCapacityExplanation": { - "type": "text", - "placeholders": { - "roomType": {} - } - }, "enterNumber": "Please enter a whole number value.", "buildTranslation": "Build your translation from the choices above", "noDatabaseEncryption": "Database encryption is not supported on this platform", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index acfcdee46..a833f813c 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4576,14 +4576,8 @@ "roomDataMissing": "Es posible que falten algunos datos de las salas de las que no es miembro.", "suggestToChat": "Sugerir este chat", "suggestToChatDesc": "Los chats sugeridos aparecerán en las listas de chats", - "roomCapacity": "Capacidad de la sala", "roomFull": "Esta sala ya está al límite de su capacidad.", "topicNotSet": "El tema no se ha fijado.", - "capacityNotSet": "Esta sala no tiene límite de capacidad.", - "roomCapacityHasBeenChanged": "Capacidad de la sala modificada", - "roomExceedsCapacity": "La sala supera su capacidad. Considere la posibilidad de retirar a los alumnos de la sala o de aumentar la capacidad.", - "capacitySetTooLow": "La capacidad de la sala no puede fijarse por debajo del número actual de no administradores.", - "roomCapacityExplanation": "La capacidad de la sala limita el número de personas que pueden entrar en ella.", "enterNumber": "Introduzca un valor numérico entero.", "autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística", "autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes", @@ -4769,7 +4763,6 @@ "conversationBotCustomZone_customSystemPromptPlaceholder": "Establecer mensaje del sistema personalizado", "conversationBotCustomZone_customSystemPromptEmptyError": "Falta mensaje del sistema personalizado", "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responde a la reacción ⏩", - "botConfig": "Configuraciones del Bot de Conversación", "addConversationBotDialogTitleInvite": "Confirmar la invitación del bot de conversación", "addConversationBotButtonInvite": "Invitar", "addConversationBotDialogInviteConfirmation": "Invitar", @@ -4871,5 +4864,148 @@ "l2SupportNa": "Haz clic en la palabra seleccionada para deseleccionarla", "l2SupportAlpha": "Alfa", "l2SupportBeta": "Beta", - "l2SupportFull": "Lleno" -} \ No newline at end of file + "l2SupportFull": "Lleno", + "chatCapacity": "Capacidad de chat", + "spaceCapacity": "Capacidad espacial", + "chatCapacityHasBeenChanged": "Capacidad de chat modificada", + "spaceCapacityHasBeenChanged": "Capacidad espacial modificada", + "chatCapacitySetTooLow": "La capacidad del chat no se puede establecer por debajo del número actual de no administradores.", + "spaceCapacitySetTooLow": "La capacidad de espacio no puede fijarse por debajo del número actual de no administradores.", + "chatCapacityExplanation": "La capacidad del chat limita el número de usuarios no administradores permitidos en un chat.", + "spaceCapacityExplanation": "La capacidad del espacio limita el número de no administradores permitidos en un espacio.", + "tooManyRequest": "Demasiadas solicitudes, por favor inténtelo más tarde.", + "voiceNotAvailable": "Parece que no tienes una voz instalada para este idioma.", + "openVoiceSettings": "Haz clic aquí para abrir los ajustes de voz", + "playAudio": "Jugar", + "stop": "Stop", + "grammarCopySCONJ": "Conjunción subordinante", + "grammarCopyNUM": "Número", + "grammarCopyVERB": "Verbo", + "grammarCopyAFFIX": "Coloque", + "grammarCopyPARTpos": "Partículas", + "grammarCopyADJ": "Adjetivo", + "grammarCopyCCONJ": "Conjunción de coordinación", + "grammarCopyPUNCT": "Puntuación", + "grammarCopyADV": "Adverbio", + "grammarCopyAUX": "Auxiliar", + "grammarCopySPACE": "Espacio", + "grammarCopySYM": "Símbolo", + "grammarCopyDET": "Determinante", + "grammarCopyPRON": "Pronombre", + "grammarCopyADP": "Adposición", + "grammarCopyPROPN": "Nombre propio", + "grammarCopyNOUN": "Sustantivo", + "grammarCopyINTJ": "Interjección", + "grammarCopyX": "Otros", + "grammarCopyFem": "Femenino", + "grammarCopy2": "Segunda persona", + "grammarCopyImp": "Imperativo", + "grammarCopyQest": "Pregunta", + "grammarCopyPerf": "Perfecto", + "grammarCopyAccNom": "Acusativo, Nominativo", + "grammarCopyObl": "Caso oblicuo", + "grammarCopyAct": "Activo", + "grammarCopyBrck": "Soporte", + "grammarCopyArt": "Artículo", + "grammarCopySing": "Singular", + "grammarCopyMasc": "Hombre", + "grammarCopyMod": "Modal", + "grammarCopyAdverbial": "Adverbial", + "grammarCopyPeri": "Perifrástico", + "grammarCopyDigit": "Dígitos", + "grammarCopyNot_proper": "No procede", + "grammarCopyCard": "Cardenal", + "grammarCopyProp": "Adecuado", + "grammarCopyDash": "Dash", + "grammarCopyYes": "Sí", + "grammarCopySemi": "Punto y coma", + "grammarCopyComm": "Coma", + "grammarCopyCnd": "Condicional", + "grammarCopyIntRel": "Interrogativo, relativo", + "grammarCopyAcc": "Acusativo", + "grammarCopyPartTag": "Partitivo", + "grammarCopyInt": "Preguntas", + "grammarCopyPast": "Anterior", + "grammarCopySup": "Superlativo", + "grammarCopyColo": "Colon", + "grammarCopy3": "Tercera persona", + "grammarCopyPlur": "Plural", + "grammarCopyNpr": "Nombre propio", + "grammarCopyInterrogative": "Preguntas", + "grammarCopyInfm": "Informal", + "grammarCopyTim": "Tiempo", + "grammarCopyNeg": "Negativo", + "grammarCopyTot": "Total", + "grammarCopyAdnomial": "Adnominal", + "grammarCopyProg": "Progresiva", + "grammarCopySub": "Subjuntivo", + "grammarCopyComplementive": "Complementive", + "grammarCopyNom": "Nominativo", + "grammarCopyFut": "Futuro", + "grammarCopyDat": "Dativo", + "grammarCopyPres": "Presente", + "grammarCopyNeut": "Esterilizar", + "grammarCopyRel": "Relativa", + "grammarCopyFinal_ending": "Final", + "grammarCopyDem": "Demostrativo", + "grammarCopyPre": "Preposición", + "grammarCopyFin": "Finito", + "grammarCopyPos": "Positivo", + "grammarCopyQuot": "Presupuesto", + "grammarCopyGer": "Redondo", + "grammarCopyPass": "Pasivo", + "grammarCopyGen": "Genitivo", + "grammarCopyPrs": "Presente", + "grammarCopyDef": "Definitivo", + "grammarCopyOrd": "Ordinal", + "grammarCopyIns": "Instrumental", + "grammarCopyAccDat": "Acusativo, Dativo", + "grammarCopyInf": "Infinitivo", + "grammarCopyLong": "Largo", + "grammarCopyInd": "Indicativo", + "grammarCopyCmp": "Comparativa", + "grammarCopyRelative_case": "Caso relativo", + "grammarCopyExcl": "Exclamativo", + "grammarCopy1": "En primera persona", + "grammarCopyIni": "Inicial", + "grammarCopyPerson": "Persona", + "grammarCopyForeign": "Extranjero", + "grammarCopyVoice": "Voz", + "grammarCopyVerbType": "Tipo de verbo", + "grammarCopyPoss": "Posesivo", + "grammarCopyPrepCase": "Caso preposicional", + "grammarCopyNumType": "Tipo de número", + "grammarCopyNounType": "Tipo de sustantivo", + "grammarCopyReflex": "Reflexivo", + "grammarCopyPronType": "Tipo de pronombre", + "grammarCopyPunctSide": "Puntuación Lado", + "grammarCopyVerbForm": "Forma verbal", + "grammarCopyGender": "Género", + "grammarCopyMood": "Estado de ánimo", + "grammarCopyAspect": "Aspecto", + "grammarCopyPunctType": "Tipo de puntuación", + "grammarCopyTense": "Tense", + "grammarCopyDegree": "Titulación", + "grammarCopyPolite": "Cortesía", + "grammarCopyAdvType": "Tipo de adverbio", + "grammarCopyNumber": "Número", + "grammarCopyConjType": "Tipo de conjunción", + "grammarCopyPolarity": "Polaridad", + "grammarCopyNumberPsor": "Número del poseedor", + "grammarCopyCase": "Caso", + "grammarCopyDefinite": "Definitividad", + "grammarCopyNumForm": "Forma numérica", + "grammarCopyUnknown": "Desconocido", + "enterPrompt": "Introduzca un mensaje del sistema", + "selectBotLanguage": "Selecciona el idioma del bot", + "chooseVoice": "Elige una voz", + "enterLanguageLevel": "Introduzca un nivel de idioma", + "enterDiscussionTopic": "Introduzca un tema de debate", + "selectBotChatMode": "Selecciona el modo de chat", + "messageNotInTargetLang": "El mensaje no está en la lengua de llegada", + "botConfig": "Configuración del chat", + "chatCapacityNotSet": "Este chat no tiene límite de capacidad.", + "spaceCapacityNotSet": "Este espacio no tiene límite de capacidad.", + "chatExceedsCapacity": "Este chat supera su capacidad.", + "spaceExceedsCapacity": "Este espacio supera su capacidad." +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart index ff7d0068a..cea57995f 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart @@ -52,7 +52,7 @@ class ClassDescriptionButton extends StatelessWidget { title: Text( room.isSpace ? L10n.of(context)!.classDescription - : L10n.of(context)!.chatTopic, + : L10n.of(context)!.chatDescription, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, @@ -74,7 +74,7 @@ void setClassTopic(Room room, BuildContext context) { title: Text( room.isSpace ? L10n.of(context)!.classDescription - : L10n.of(context)!.chatTopic, + : L10n.of(context)!.chatDescription, ), content: TextField( controller: textFieldController, diff --git a/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart index 11452830c..5b22c793c 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart @@ -62,21 +62,17 @@ class RoomCapacityButtonState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - L10n.of(context)!.roomExceedsCapacity, + spaceMode + ? L10n.of(context)!.chatExceedsCapacity + : L10n.of(context)!.spaceExceedsCapacity, ), ), ); } } - String get roomType { - final String chat = L10n.of(context)!.chat; - final String space = L10n.of(context)!.space; - if (widget.room != null) { - return widget.room!.isSpace ? space : chat; - } - return widget.spaceMode ? space : chat; - } + bool get spaceMode => + (widget.room != null && widget.room!.isSpace) || widget.spaceMode; @override Widget build(BuildContext context) { @@ -92,13 +88,17 @@ class RoomCapacityButtonState extends State { ), subtitle: Text( (capacity == null) - ? L10n.of(context)!.capacityNotSet + ? spaceMode + ? L10n.of(context)!.spaceCapacityNotSet + : L10n.of(context)!.chatCapacityNotSet : (nonAdmins != null) ? '$nonAdmins/$capacity' : '$capacity', ), title: Text( - L10n.of(context)!.roomCapacity(roomType), + spaceMode + ? L10n.of(context)!.spaceCapacity + : L10n.of(context)!.chatCapacity, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, @@ -116,8 +116,12 @@ class RoomCapacityButtonState extends State { Future setRoomCapacity() async { final input = await showTextInputDialog( context: context, - title: L10n.of(context)!.roomCapacity(roomType), - message: L10n.of(context)!.roomCapacityExplanation(roomType), + title: spaceMode + ? L10n.of(context)!.spaceCapacity + : L10n.of(context)!.chatCapacity, + message: spaceMode + ? L10n.of(context)!.spaceCapacityExplanation + : L10n.of(context)!.chatCapacityExplanation, okLabel: L10n.of(context)!.ok, cancelLabel: L10n.of(context)!.cancel, textFields: [ @@ -133,7 +137,9 @@ class RoomCapacityButtonState extends State { return L10n.of(context)!.enterNumber; } if (nonAdmins != null && int.parse(value) < int.parse(nonAdmins!)) { - return L10n.of(context)!.capacitySetTooLow(roomType); + return spaceMode + ? L10n.of(context)!.spaceCapacitySetTooLow + : L10n.of(context)!.chatCapacitySetTooLow; } return null; }, @@ -159,7 +165,9 @@ class RoomCapacityButtonState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - L10n.of(context)!.roomCapacityHasBeenChanged(roomType), + spaceMode + ? L10n.of(context)!.spaceCapacityHasBeenChanged + : L10n.of(context)!.chatCapacityHasBeenChanged, ), ), ); From d1d078e2b80d2f7fa788b0a43a232566bbf88227 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 30 Oct 2024 12:43:07 -0400 Subject: [PATCH 103/115] set auto-play IT to true by default --- lib/pangea/constants/model_keys.dart | 2 +- lib/pangea/models/user_model.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 424c07830..253d73e55 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -24,7 +24,7 @@ class ModelKey { // making this a random string so that it's harder to guess static const String activatedTrialKey = '7C4EuKIsph'; static const String autoPlayMessages = 'autoPlayMessages'; - static const String itAutoPlay = 'itAutoPlay'; + static const String itAutoPlay = 'autoPlayIT'; static const String clientClassCity = "city"; static const String clientClassCountry = "country"; diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 7ef85fbd5..515b64088 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -23,7 +23,7 @@ class UserSettings { this.dateOfBirth, this.createdAt, this.autoPlayMessages = false, - this.itAutoPlay = false, + this.itAutoPlay = true, this.activatedFreeTrial = false, this.publicProfile = false, this.targetLanguage, @@ -37,7 +37,7 @@ class UserSettings { ? DateTime.parse(json[ModelKey.userCreatedAt]) : null, autoPlayMessages: json[ModelKey.autoPlayMessages] ?? false, - itAutoPlay: json[ModelKey.itAutoPlay] ?? false, + itAutoPlay: json[ModelKey.itAutoPlay] ?? true, activatedFreeTrial: json[ModelKey.activatedTrialKey] ?? false, publicProfile: json[ModelKey.publicProfile] ?? false, targetLanguage: json[ModelKey.l2LanguageKey], @@ -98,7 +98,7 @@ class UserSettings { false, itAutoPlay: (accountData[ModelKey.itAutoPlay] ?.content[ModelKey.itAutoPlay] as bool?) ?? - false, + true, activatedFreeTrial: (accountData[ModelKey.activatedTrialKey] ?.content[ModelKey.activatedTrialKey] as bool?) ?? false, From 7f9c0047fb5ac9c8d3e80fa56a26ef46a7d0818d Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 30 Oct 2024 15:22:34 -0400 Subject: [PATCH 104/115] no xp for ign, more minus xp for inc, dont show 0 xp constructs --- lib/pangea/enum/construct_use_type_enum.dart | 8 ++++---- .../analytics_summary/analytics_popup.dart | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index 6ced270b7..196cf89b4 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -105,23 +105,23 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return 2; case ConstructUseTypeEnum.corIt: + return 1; + case ConstructUseTypeEnum.ignIt: case ConstructUseTypeEnum.ignIGC: case ConstructUseTypeEnum.ignPA: case ConstructUseTypeEnum.ignWL: - return 1; - case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: return 0; case ConstructUseTypeEnum.incIt: case ConstructUseTypeEnum.incIGC: - return -1; + return -2; case ConstructUseTypeEnum.incPA: case ConstructUseTypeEnum.incWL: - return -2; + return -3; } } } diff --git a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart index 9dcc7b06b..7f609ce11 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart @@ -16,6 +16,11 @@ class AnalyticsPopup extends StatelessWidget { super.key, }); + // we just want to show the constructs that have points + List get constructs => constructsModel.constructList + .where((constructUse) => constructUse.points > 0) + .toList(); + @override Widget build(BuildContext context) { return Dialog( @@ -36,16 +41,16 @@ class AnalyticsPopup extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.symmetric(vertical: 20), - child: constructsModel.constructList.isEmpty + child: constructs.isEmpty ? Center( child: Text(L10n.of(context)!.noDataFound), ) : ListView.builder( - itemCount: constructsModel.constructList.length, + itemCount: constructs.length, itemBuilder: (context, index) { return Tooltip( message: - "${constructsModel.constructList[index].points} / ${constructsModel.maxXPPerLemma}", + "${constructs[index].points} / ${constructsModel.maxXPPerLemma}", child: ListTile( onTap: () {}, title: Text( @@ -55,12 +60,11 @@ class AnalyticsPopup extends StatelessWidget { .constructList[index].lemma, context, ) - : constructsModel.constructList[index].lemma, + : constructs[index].lemma, ), subtitle: LinearProgressIndicator( - value: - constructsModel.constructList[index].points / - constructsModel.maxXPPerLemma, + value: constructs[index].points / + constructsModel.maxXPPerLemma, minHeight: 20, borderRadius: const BorderRadius.all( Radius.circular(AppConfig.borderRadius), From b5fd9bed4bfc36135ccd07bd56755fe8bb9ba537 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Thu, 31 Oct 2024 17:18:05 +0700 Subject: [PATCH 105/115] fix bot not responding when new user creates an account and start dm with bot --- lib/pangea/controllers/pangea_controller.dart | 83 ++++++++++++++++++- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 3f0938052..c24e8b3d2 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'dart:math'; +import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/class_controller.dart'; @@ -22,6 +23,7 @@ import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; +import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/instructions.dart'; @@ -205,15 +207,88 @@ class PangeaController { if (botDMs.isEmpty) { try { - await matrixState.client.startDirectChat( - BotName.byEnvironment, - enableEncryption: false, + // Copied from client.dart.startDirectChat + final directChatRoomId = + matrixState.client.getDirectChatFromUserId(BotName.byEnvironment); + if (directChatRoomId != null) { + final room = matrixState.client.getRoomById(directChatRoomId); + if (room != null) { + if (room.membership == Membership.join) { + return null; + } else if (room.membership == Membership.invite) { + // we might already have an invite into a DM room. If that is the case, we should try to join. If the room is + // unjoinable, that will automatically leave the room, so in that case we need to continue creating a new + // room. (This implicitly also prevents the room from being returned as a DM room by getDirectChatFromUserId, + // because it only returns joined or invited rooms atm.) + await room.join(); + if (room.membership != Membership.leave) { + if (room.membership != Membership.join) { + // Wait for room actually appears in sync with the right membership + await matrixState.client + .waitForRoomInSync(directChatRoomId, join: true); + } + return null; + } + } + } + } + // enableEncryption ??= + // encryptionEnabled && await userOwnsEncryptionKeys(mxid); + // if (enableEncryption) { + // initialState ??= []; + // if (!initialState.any((s) => s.type == EventTypes.Encryption)) { + // initialState.add( + // StateEvent( + // content: { + // 'algorithm': supportedGroupEncryptionAlgorithms.first, + // }, + // type: EventTypes.Encryption, + // ), + // ); + // } + // } + + // Start a new direct chat + final roomId = await matrixState.client.createRoom( + invite: [], // intentionally not invite bot yet + isDirect: true, + preset: CreateRoomPreset.trustedPrivateChat, + initialState: [ + BotOptionsModel(mode: BotMode.directChat).toStateEvent, + ], ); + + final room = matrixState.client.getRoomById(roomId); + if (room == null || room.membership != Membership.join) { + // Wait for room actually appears in sync + await matrixState.client.waitForRoomInSync(roomId, join: true); + } + + final botOptions = room!.getState(PangeaEventTypes.botOptions); + if (botOptions == null) { + await matrixState.client.setRoomStateWithKey( + roomId, + PangeaEventTypes.botOptions, + "", + BotOptionsModel(mode: BotMode.directChat).toJson(), + ); + await matrixState.client + .getRoomStateWithKey(roomId, PangeaEventTypes.botOptions, ""); + } + + // invite bot to direct chat + await matrixState.client.setRoomStateWithKey( + roomId, EventTypes.RoomMember, BotName.byEnvironment, { + "membership": Membership.invite.name, + "is_direct": true, + }); + await room.addToDirectChat(BotName.byEnvironment); + + return null; } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); } - return; } final Room botDMWithLatestActivity = botDMs.reduce((a, b) { From a7b6003c89fc57dad010278f7690a01ea2169e6b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 11:17:51 -0400 Subject: [PATCH 106/115] comment out references to itAutoPlay setting --- .../controllers/choreographer.dart | 6 ++--- .../controllers/igc_controller.dart | 2 +- lib/pangea/models/user_model.dart | 14 +++++----- .../settings_learning_view.dart | 26 +++++++++---------- .../widgets/igc/pangea_text_controller.dart | 2 +- lib/pangea/widgets/igc/span_card.dart | 20 +++++++------- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 6f65ea836..edb364f80 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -536,9 +536,9 @@ class Choreographer { chatController.room, ); - bool get itAutoPlayEnabled { - return pangeaController.userController.profile.userSettings.itAutoPlay; - } + // bool get itAutoPlayEnabled { + // return pangeaController.userController.profile.userSettings.itAutoPlay; + // } bool get definitionsEnabled => pangeaController.permissionsController.isToolEnabled( diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index ed770cca4..6d0e848aa 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -99,7 +99,7 @@ class IgcController { final PangeaMatch match = igcTextData!.matches[firstMatchIndex]; if (match.isITStart && - choreographer.itAutoPlayEnabled && + // choreographer.itAutoPlayEnabled && igcTextData != null) { choreographer.onITStart(igcTextData!.matches[firstMatchIndex]); return; diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 515b64088..1fdebef3a 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -12,7 +12,7 @@ class UserSettings { DateTime? dateOfBirth; DateTime? createdAt; bool autoPlayMessages; - bool itAutoPlay; + // bool itAutoPlay; bool activatedFreeTrial; bool publicProfile; String? targetLanguage; @@ -23,7 +23,7 @@ class UserSettings { this.dateOfBirth, this.createdAt, this.autoPlayMessages = false, - this.itAutoPlay = true, + // this.itAutoPlay = true, this.activatedFreeTrial = false, this.publicProfile = false, this.targetLanguage, @@ -37,7 +37,7 @@ class UserSettings { ? DateTime.parse(json[ModelKey.userCreatedAt]) : null, autoPlayMessages: json[ModelKey.autoPlayMessages] ?? false, - itAutoPlay: json[ModelKey.itAutoPlay] ?? true, + // itAutoPlay: json[ModelKey.itAutoPlay] ?? true, activatedFreeTrial: json[ModelKey.activatedTrialKey] ?? false, publicProfile: json[ModelKey.publicProfile] ?? false, targetLanguage: json[ModelKey.l2LanguageKey], @@ -50,7 +50,7 @@ class UserSettings { data[ModelKey.userDateOfBirth] = dateOfBirth?.toIso8601String(); data[ModelKey.userCreatedAt] = createdAt?.toIso8601String(); data[ModelKey.autoPlayMessages] = autoPlayMessages; - data[ModelKey.itAutoPlay] = itAutoPlay; + // data[ModelKey.itAutoPlay] = itAutoPlay; data[ModelKey.activatedTrialKey] = activatedFreeTrial; data[ModelKey.publicProfile] = publicProfile; data[ModelKey.l2LanguageKey] = targetLanguage; @@ -96,9 +96,9 @@ class UserSettings { autoPlayMessages: (accountData[ModelKey.autoPlayMessages] ?.content[ModelKey.autoPlayMessages] as bool?) ?? false, - itAutoPlay: (accountData[ModelKey.itAutoPlay] - ?.content[ModelKey.itAutoPlay] as bool?) ?? - true, + // itAutoPlay: (accountData[ModelKey.itAutoPlay] + // ?.content[ModelKey.itAutoPlay] as bool?) ?? + // true, activatedFreeTrial: (accountData[ModelKey.activatedTrialKey] ?.content[ModelKey.activatedTrialKey] as bool?) ?? false, diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart index 943d81ca2..13dd8d4d7 100644 --- a/lib/pangea/pages/settings_learning/settings_learning_view.dart +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -48,19 +48,19 @@ class SettingsLearningView extends StatelessWidget { value, ), ), - ProfileSettingsSwitchListTile.adaptive( - defaultValue: controller.pangeaController.userController.profile - .userSettings.itAutoPlay, - title: - L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader, - subtitle: L10n.of(context)!.interactiveTranslatorAutoPlayDesc, - onChange: (bool value) => controller - .pangeaController.userController - .updateProfile((profile) { - profile.userSettings.itAutoPlay = value; - return profile; - }), - ), + // ProfileSettingsSwitchListTile.adaptive( + // defaultValue: controller.pangeaController.userController.profile + // .userSettings.itAutoPlay, + // title: + // L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader, + // subtitle: L10n.of(context)!.interactiveTranslatorAutoPlayDesc, + // onChange: (bool value) => controller + // .pangeaController.userController + // .updateProfile((profile) { + // profile.userSettings.itAutoPlay = value; + // return profile; + // }), + // ), // ProfileSettingsSwitchListTile.adaptive( // defaultValue: controller.pangeaController.userController.profile // .userSettings.autoPlayMessages, diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index b7bbc1af8..a8ad07d71 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -90,7 +90,7 @@ class PangeaTextController extends TextEditingController { // if autoplay on and it start then just start it if (matchIndex != -1 && - choreographer.itAutoPlayEnabled && + // choreographer.itAutoPlayEnabled && choreographer.igc.igcTextData!.matches[matchIndex].isITStart) { return choreographer.onITStart( choreographer.igc.igcTextData!.matches[matchIndex], diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index ddfa43ba4..63ecad332 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -355,16 +355,16 @@ class WordMatchContent extends StatelessWidget { ), ], ), - if (controller.widget.scm.pangeaMatch!.isITStart) - DontShowSwitchListTile( - controller: pangeaController, - onSwitch: (bool value) { - pangeaController.userController.updateProfile((profile) { - profile.userSettings.itAutoPlay = value; - return profile; - }); - }, - ), + // if (controller.widget.scm.pangeaMatch!.isITStart) + // DontShowSwitchListTile( + // controller: pangeaController, + // onSwitch: (bool value) { + // pangeaController.userController.updateProfile((profile) { + // profile.userSettings.itAutoPlay = value; + // return profile; + // }); + // }, + // ), ], ), ], From 8e4bf109ed409e76e1c9a6e7f2926c5a2b874bae Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Thu, 31 Oct 2024 22:58:59 +0700 Subject: [PATCH 107/115] fix brand new account not using new function --- lib/pangea/pages/p_user_age/p_user_age.dart | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart index fd937485c..d7e36f200 100644 --- a/lib/pangea/pages/p_user_age/p_user_age.dart +++ b/lib/pangea/pages/p_user_age/p_user_age.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/constants/age_limits.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; import 'package:fluffychat/pangea/pages/p_user_age/p_user_age_view.dart'; import 'package:fluffychat/pangea/utils/p_extension.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; @@ -11,7 +10,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../../utils/bot_name.dart'; import '../../utils/error_handler.dart'; class PUserAge extends StatefulWidget { @@ -34,20 +32,7 @@ class PUserAgeController extends State { @override void initState() { super.initState(); - Future.delayed(Duration.zero, () async { - if (!(await Matrix.of(context).client.hasBotDM)) { - Matrix.of(context) - .client - .startDirectChat( - BotName.byEnvironment, - enableEncryption: false, - ) - .onError( - (error, stackTrace) => - ErrorHandler.logError(e: error, s: stackTrace), - ); - } - }); + pangeaController.startChatWithBotIfNotPresent(); } String? dobValidator() { From 79a6c300d639ffd82a6208f411f36adacb54a002 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 31 Oct 2024 12:52:05 -0400 Subject: [PATCH 108/115] copy for Loc --- assets/l10n/intl_en.arb | 1 + assets/l10n/intl_es.arb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 1cd522a5c..b5310c8e3 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4303,6 +4303,7 @@ "grammarCopyAccDat": "Accusative, Dative", "grammarCopyInf": "Infinitive", "grammarCopyLong": "Long", + "grammarCopyLoc": "Locative", "grammarCopyInd": "Indicative", "grammarCopyCmp": "Comparative", "grammarCopyRelative_case": "Relative Case", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index a833f813c..fd478e870 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4962,6 +4962,7 @@ "grammarCopyAccDat": "Acusativo, Dativo", "grammarCopyInf": "Infinitivo", "grammarCopyLong": "Largo", + "grammarCopyLoc": "Locativa", "grammarCopyInd": "Indicativo", "grammarCopyCmp": "Comparativa", "grammarCopyRelative_case": "Caso relativo", @@ -5008,4 +5009,4 @@ "spaceCapacityNotSet": "Este espacio no tiene límite de capacidad.", "chatExceedsCapacity": "Este chat supera su capacidad.", "spaceExceedsCapacity": "Este espacio supera su capacidad." -} +} \ No newline at end of file From 307eb92227b55372e27112758ed54ca4416be6ee Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 13:30:51 -0400 Subject: [PATCH 109/115] allow text to wrap in inline tooltips --- lib/pangea/utils/inline_tooltip.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart index 4c81e1bbd..f96cc05bb 100644 --- a/lib/pangea/utils/inline_tooltip.dart +++ b/lib/pangea/utils/inline_tooltip.dart @@ -41,14 +41,16 @@ class InlineTooltip extends StatelessWidget { ), const SizedBox(width: 8), // Text in the middle - Center( - child: Text( - instructionsEnum.body(L10n.of(context)!), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - height: 1.5, + Flexible( + child: Center( + child: Text( + instructionsEnum.body(L10n.of(context)!), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + height: 1.5, + ), + textAlign: TextAlign.left, ), - textAlign: TextAlign.left, ), ), // Close button on the right From 646272eae0cb491505fdc1bed516b89f16544cb3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 14:10:38 -0400 Subject: [PATCH 110/115] in conversation bot settings dialog, on tap outside textfield close mobile keyboard --- .../conversation_bot/conversation_bot_mode_dynamic_zone.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index 5a3082610..e691cbcb5 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -24,6 +24,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { Widget build(BuildContext context) { final discussionChildren = [ TextFormField( + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotDiscussionZone_discussionTopicPlaceholder, @@ -43,6 +44,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { ), const SizedBox(height: 12), TextFormField( + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotDiscussionZone_discussionKeywordsPlaceholder, @@ -58,6 +60,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget { final customChildren = [ TextFormField( + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), decoration: InputDecoration( hintText: L10n.of(context)! .conversationBotCustomZone_customSystemPromptPlaceholder, From aab41e260025291f778e337e1118138a8d8153a9 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 14:15:53 -0400 Subject: [PATCH 111/115] remove room details button because it's duplicate of button next to avatar --- lib/pages/chat_details/chat_details_view.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 90f2a8927..72441e450 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pangea/pages/class_settings/class_name_header.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart'; import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart'; import 'package:fluffychat/pangea/utils/lock_room.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart'; @@ -213,11 +212,6 @@ class ChatDetailsView extends StatelessWidget { ), Divider(color: theme.dividerColor), // #Pangea - if (room.isRoomAdmin) - ClassNameButton( - room: room, - controller: controller, - ), if (room.canSendEvent('m.room.topic')) ClassDescriptionButton( room: room, From 662098ee4c7fda6691a02e20f35e90ba86b7627b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 14:46:48 -0400 Subject: [PATCH 112/115] use filtered construct list and number of lemmas in learning analytics view --- .../analytics/construct_list_model.dart | 7 +++++++ .../analytics_summary/analytics_popup.dart | 19 ++++++++----------- .../learning_progress_indicators.dart | 4 ++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/pangea/models/analytics/construct_list_model.dart b/lib/pangea/models/analytics/construct_list_model.dart index d73b5060a..23763824c 100644 --- a/lib/pangea/models/analytics/construct_list_model.dart +++ b/lib/pangea/models/analytics/construct_list_model.dart @@ -26,6 +26,10 @@ class ConstructListModel { /// All unique lemmas used in the construct events List get lemmas => constructList.map((e) => e.lemma).toSet().toList(); + /// All unique lemmas used in the construct events with non-zero points + List get lemmasWithPoints => + constructListWithPoints.map((e) => e.lemma).toSet().toList(); + /// A map of lemmas to ConstructUses, each of which contains a lemma /// key = lemmma + constructType.string, value = ConstructUses void _buildConstructMap() { @@ -72,6 +76,9 @@ class ConstructListModel { return _constructList!; } + List get constructListWithPoints => + constructList.where((constructUse) => constructUse.points > 0).toList(); + get maxXPPerLemma { return type != null ? type!.maxXPPerLemma diff --git a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart index 7f609ce11..d7978af73 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/analytics_popup.dart @@ -16,11 +16,6 @@ class AnalyticsPopup extends StatelessWidget { super.key, }); - // we just want to show the constructs that have points - List get constructs => constructsModel.constructList - .where((constructUse) => constructUse.points > 0) - .toList(); - @override Widget build(BuildContext context) { return Dialog( @@ -41,29 +36,31 @@ class AnalyticsPopup extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.symmetric(vertical: 20), - child: constructs.isEmpty + child: constructsModel.constructListWithPoints.isEmpty ? Center( child: Text(L10n.of(context)!.noDataFound), ) : ListView.builder( - itemCount: constructs.length, + itemCount: constructsModel.constructListWithPoints.length, itemBuilder: (context, index) { return Tooltip( message: - "${constructs[index].points} / ${constructsModel.maxXPPerLemma}", + "${constructsModel.constructListWithPoints[index].points} / ${constructsModel.maxXPPerLemma}", child: ListTile( onTap: () {}, title: Text( constructsModel.type == ConstructTypeEnum.morph ? getGrammarCopy( constructsModel - .constructList[index].lemma, + .constructListWithPoints[index].lemma, context, ) - : constructs[index].lemma, + : constructsModel + .constructListWithPoints[index].lemma, ), subtitle: LinearProgressIndicator( - value: constructs[index].points / + value: constructsModel + .constructListWithPoints[index].points / constructsModel.maxXPPerLemma, minHeight: 20, borderRadius: const BorderRadius.all( 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 6695d2673..0a616c6bd 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 @@ -111,9 +111,9 @@ class LearningProgressIndicatorsState int? getProgressPoints(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.wordsUsed: - return words?.lemmas.length; + return words?.lemmasWithPoints.length; case ProgressIndicatorEnum.morphsUsed: - return morphs?.lemmas.length; + return morphs?.lemmasWithPoints.length; case ProgressIndicatorEnum.level: return level; } From a0faa997306aba4043cfca0a03ab27d4cf3edcea Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 15:01:56 -0400 Subject: [PATCH 113/115] restrict width of missing voice button --- .../widgets/chat/missing_voice_button.dart | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart index b1f12c626..1765a9d20 100644 --- a/lib/pangea/widgets/chat/missing_voice_button.dart +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -40,23 +40,27 @@ class MissingVoiceButton extends StatelessWidget { ), padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(top: 8), - child: Column( - 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: SizedBox( + width: AppConfig.toolbarMinWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context)!.voiceNotAvailable, + textAlign: TextAlign.center, ), - child: Text(L10n.of(context)!.openVoiceSettings), - ), - ], + 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), + ), + ], + ), ), ); } From 80d9276f6e393e42281619a838c70e994e5b034c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 15:44:41 -0400 Subject: [PATCH 114/115] don't allow user to chose the incorrect choices in IT --- lib/pangea/choreographer/widgets/it_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 9b81eb960..529100d94 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -352,7 +352,7 @@ class ITChoices extends StatelessWidget { void selectContinuance(int index, BuildContext context) { final Continuance continuance = controller.currentITStep!.continuances[index]; - if (continuance.level == 1 || continuance.wasClicked) { + if (continuance.level == 1) { Future.delayed( const Duration(milliseconds: 500), () => controller.selectTranslation(index), From bda5cd4728336e705cc5545ba08bd81ce396d073 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 31 Oct 2024 16:42:33 -0400 Subject: [PATCH 115/115] updated chat description copy and added padding to move scrollbar out of bot settings box --- .../class_description_button.dart | 2 +- .../conversation_bot_settings.dart | 44 ++++++++++--------- pubspec.yaml | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart index cea57995f..35d787fce 100644 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart @@ -42,7 +42,7 @@ class ClassDescriptionButton extends StatelessWidget { ? (room.isRoomAdmin ? (room.isSpace ? L10n.of(context)!.classDescriptionDesc - : L10n.of(context)!.chatTopicDesc) + : L10n.of(context)!.setChatDescription) : L10n.of(context)!.topicNotSet) : room.topic, ), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart index 044fcc214..e30429214 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_settings.dart @@ -200,27 +200,31 @@ class ConversationBotSettingsDialogState ), Expanded( child: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 20), - AnimatedOpacity( - duration: FluffyThemes.animationDuration, - opacity: addBot ? 1.0 : 0.5, - child: ConversationBotSettingsForm( - botOptions: botOptions, - discussionKeywordsController: - discussionKeywordsController, - discussionTopicController: discussionTopicController, - customSystemPromptController: - customSystemPromptController, - enabled: addBot, - onUpdateBotMode: onUpdateChatMode, - onUpdateBotLanguage: onUpdateBotLanguage, - onUpdateBotVoice: onUpdateBotVoice, - onUpdateBotLanguageLevel: onUpdateBotLanguageLevel, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 20), + AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: addBot ? 1.0 : 0.5, + child: ConversationBotSettingsForm( + botOptions: botOptions, + discussionKeywordsController: + discussionKeywordsController, + discussionTopicController: + discussionTopicController, + customSystemPromptController: + customSystemPromptController, + enabled: addBot, + onUpdateBotMode: onUpdateChatMode, + onUpdateBotLanguage: onUpdateBotLanguage, + onUpdateBotVoice: onUpdateBotVoice, + onUpdateBotLanguageLevel: onUpdateBotLanguageLevel, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 788d4d1ca..8e7242893 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.23.2+3561 +version: 1.23.3+3562 environment: sdk: ">=3.0.0 <4.0.0"