Merge branch 'main' into move-server-main

This commit is contained in:
Brord van Wierst 2024-11-03 17:42:48 +01:00
commit b1524bc7e7
No known key found for this signature in database
GPG key ID: 0922A3CA225DF791
36 changed files with 596 additions and 356 deletions

View file

@ -4337,6 +4337,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",

View file

@ -691,6 +691,7 @@ class ChatController extends State<ChatPageWithRoom>
metadata: metadata,
)),
],
origin: AnalyticsUpdateOrigin.sendMessage,
),
);
}

View file

@ -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#
),
],

View file

@ -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(

View file

@ -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#
),
),
);

View file

@ -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(
[

View file

@ -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#
),
),
),
),

View file

@ -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:

View file

@ -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 =

View file

@ -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(

View file

@ -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;

View file

@ -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,
),

View file

@ -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,
});
}

View file

@ -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});
}

View file

@ -5,4 +5,5 @@ enum EditType {
alternativeTranslation,
itGold,
itStart,
itDismissed,
}

View file

@ -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,

View file

@ -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,
},
);
}

View file

@ -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(

View file

@ -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();

View file

@ -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),

View file

@ -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,
),
),
],
);
},
);
}
}

View file

@ -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,
});
}

View file

@ -158,7 +158,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
} else {
audioFile = await widget.messageEvent.getMatrixAudioFile(
langCode,
context,
);
}
debugPrint("audio file is now: $audioFile. setting starts and ends...");

View file

@ -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);
@ -65,33 +68,71 @@ class OverlayMessageTextState extends State<OverlayMessageText> {
);
}
int lastEnd = 0;
// Convert the entire message into a list of characters
final Characters messageCharacters =
widget.pangeaMessageEvent.event.body.characters;
// When building token positions, use grapheme cluster indices
final List<TokenPosition> tokenPositions = [];
int globalIndex = 0;
for (int i = 0; i < tokens!.length; i++) {
final token = tokens![i];
final start = token.start;
final end = token.end;
if (lastEnd < start) {
tokenPositions.add(TokenPosition(start: lastEnd, end: start));
// Calculate the number of grapheme clusters up to the start and end positions
final int startIndex = messageCharacters.take(start).length;
final int endIndex = messageCharacters.take(end).length;
if (globalIndex < startIndex) {
tokenPositions.add(TokenPosition(start: globalIndex, end: startIndex));
}
tokenPositions.add(
TokenPosition(
start: start,
end: end,
start: startIndex,
end: endIndex,
tokenIndex: i,
token: token,
),
);
lastEnd = end;
globalIndex = endIndex;
}
// debug prints for fixing words sticking together
// void printEscapedString(String input) {
// // Escaped string using Unicode escape sequences
// final String escapedString = input.replaceAllMapped(
// RegExp(r'[^\w\s]', unicode: true),
// (match) {
// final codeUnits = match.group(0)!.runes;
// String unicodeEscapes = '';
// for (final rune in codeUnits) {
// unicodeEscapes += '\\u{${rune.toRadixString(16)}}';
// }
// return unicodeEscapes;
// },
// );
// print("Escaped String: $escapedString");
// // Printing each character with its index
// int index = 0;
// for (final char in input.characters) {
// print("Index $index: $char");
// index++;
// }
// }
//TODO - take out of build function of every message
return RichText(
text: TextSpan(
children: tokenPositions.map((tokenPosition) {
final substring = messageCharacters
.skip(tokenPosition.start)
.take(tokenPosition.end - tokenPosition.start)
.toString();
if (tokenPosition.token != null) {
final isSelected =
widget.overlayController.isTokenSelected(tokenPosition.token!);
@ -104,9 +145,9 @@ class OverlayMessageTextState extends State<OverlayMessageText> {
widget.overlayController.onClickOverlayMessageToken(
tokenPosition.token!,
);
setState(() {});
if (mounted) setState(() {});
},
text: tokenPosition.token!.text.content,
text: substring,
style: style.merge(
TextStyle(
backgroundColor: isSelected
@ -119,10 +160,7 @@ class OverlayMessageTextState extends State<OverlayMessageText> {
);
} else {
return TextSpan(
text: widget.pangeaMessageEvent.event.body.substring(
tokenPosition.start,
tokenPosition.end,
),
text: substring,
style: style,
);
}

View file

@ -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;

View file

@ -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),
],
],
),
),
],
);

View file

@ -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,
),
),
],
),
),
);

View 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);
}

View file

@ -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: [

View file

@ -70,7 +70,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 +80,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
widget.practiceCardController.currentActivity!,
widget.practiceCardController.metadata,
),
origin: AnalyticsUpdateOrigin.practiceActivity,
),
);
@ -112,7 +112,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
),
),
const SizedBox(height: 8),
// #freeze-activity
if (practiceActivity.activityType ==
ActivityTypeEnum.wordFocusListening)
WordAudioButton(

View file

@ -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,
@ -217,7 +217,6 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
currentActivity!,
metadata,
),
context,
widget.pangeaMessageEvent,
);
@ -335,7 +334,9 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
children: [
// Main content
const Positioned(
child: PointsGainedAnimation(),
child: PointsGainedAnimation(
origin: AnalyticsUpdateOrigin.practiceActivity,
),
),
if (activityWidget != null)
Padding(

View file

@ -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;
}

View file

@ -55,7 +55,6 @@ class WordAudioButtonState extends State<WordAudioButton> {
}
}, // Disable button if language isn't supported
),
// #freeze-activity
widget.ttsController.missingVoiceButton,
],
);

View file

@ -79,6 +79,7 @@ class WordFocusListeningActivityState
widget.practiceCardController.currentActivity!,
widget.practiceCardController.metadata,
),
origin: AnalyticsUpdateOrigin.practiceActivity,
),
);
setState(() {

View file

@ -162,7 +162,7 @@ packages:
source: hosted
version: "1.1.2"
characters:
dependency: transitive
dependency: "direct main"
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"

View file

@ -19,6 +19,7 @@ dependencies:
badges: ^3.1.2
blurhash_dart: ^1.2.1
callkeep: ^0.3.2
characters: ^1.2.0
chewie: ^1.8.1
collection: ^1.18.0
cupertino_icons: any