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