Merge branch 'main' into sentry

This commit is contained in:
ggurdin 2024-11-01 10:31:33 -04:00 committed by GitHub
commit a4c346b07c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 344 additions and 170 deletions

View file

@ -4303,6 +4303,7 @@
"grammarCopyAccDat": "Accusative, Dative",
"grammarCopyInf": "Infinitive",
"grammarCopyLong": "Long",
"grammarCopyLoc": "Locative",
"grammarCopyInd": "Indicative",
"grammarCopyCmp": "Comparative",
"grammarCopyRelative_case": "Relative Case",

View file

@ -4962,6 +4962,7 @@
"grammarCopyAccDat": "Acusativo, Dativo",
"grammarCopyInf": "Infinitivo",
"grammarCopyLong": "Largo",
"grammarCopyLoc": "Locativa",
"grammarCopyInd": "Indicativo",
"grammarCopyCmp": "Comparativa",
"grammarCopyRelative_case": "Caso relativo",
@ -5008,4 +5009,4 @@
"spaceCapacityNotSet": "Este espacio no tiene límite de capacidad.",
"chatExceedsCapacity": "Este chat supera su capacidad.",
"spaceExceedsCapacity": "Este espacio supera su capacidad."
}
}

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

@ -7,7 +7,6 @@ import 'package:fluffychat/pangea/pages/class_settings/class_name_header.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
import 'package:fluffychat/pangea/utils/lock_room.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
@ -213,11 +212,6 @@ class ChatDetailsView extends StatelessWidget {
),
Divider(color: theme.dividerColor),
// #Pangea
if (room.isRoomAdmin)
ClassNameButton(
room: room,
controller: controller,
),
if (room.canSendEvent('m.room.topic'))
ClassDescriptionButton(
room: room,

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(
@ -352,7 +355,7 @@ class ITChoices extends StatelessWidget {
void selectContinuance(int index, BuildContext context) {
final Continuance continuance =
controller.currentITStep!.continuances[index];
if (continuance.level == 1 || continuance.wasClicked) {
if (continuance.level == 1) {
Future.delayed(
const Duration(milliseconds: 500),
() => controller.selectTranslation(index),
@ -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

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:math';
import 'package:fluffychat/pangea/constants/bot_mode.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/class_controller.dart';
@ -22,6 +23,7 @@ import 'package:fluffychat/pangea/controllers/user_controller.dart';
import 'package:fluffychat/pangea/controllers/word_net_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/instructions.dart';
@ -205,15 +207,88 @@ class PangeaController {
if (botDMs.isEmpty) {
try {
await matrixState.client.startDirectChat(
BotName.byEnvironment,
enableEncryption: false,
// Copied from client.dart.startDirectChat
final directChatRoomId =
matrixState.client.getDirectChatFromUserId(BotName.byEnvironment);
if (directChatRoomId != null) {
final room = matrixState.client.getRoomById(directChatRoomId);
if (room != null) {
if (room.membership == Membership.join) {
return null;
} else if (room.membership == Membership.invite) {
// we might already have an invite into a DM room. If that is the case, we should try to join. If the room is
// unjoinable, that will automatically leave the room, so in that case we need to continue creating a new
// room. (This implicitly also prevents the room from being returned as a DM room by getDirectChatFromUserId,
// because it only returns joined or invited rooms atm.)
await room.join();
if (room.membership != Membership.leave) {
if (room.membership != Membership.join) {
// Wait for room actually appears in sync with the right membership
await matrixState.client
.waitForRoomInSync(directChatRoomId, join: true);
}
return null;
}
}
}
}
// enableEncryption ??=
// encryptionEnabled && await userOwnsEncryptionKeys(mxid);
// if (enableEncryption) {
// initialState ??= [];
// if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
// initialState.add(
// StateEvent(
// content: {
// 'algorithm': supportedGroupEncryptionAlgorithms.first,
// },
// type: EventTypes.Encryption,
// ),
// );
// }
// }
// Start a new direct chat
final roomId = await matrixState.client.createRoom(
invite: [], // intentionally not invite bot yet
isDirect: true,
preset: CreateRoomPreset.trustedPrivateChat,
initialState: [
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
],
);
final room = matrixState.client.getRoomById(roomId);
if (room == null || room.membership != Membership.join) {
// Wait for room actually appears in sync
await matrixState.client.waitForRoomInSync(roomId, join: true);
}
final botOptions = room!.getState(PangeaEventTypes.botOptions);
if (botOptions == null) {
await matrixState.client.setRoomStateWithKey(
roomId,
PangeaEventTypes.botOptions,
"",
BotOptionsModel(mode: BotMode.directChat).toJson(),
);
await matrixState.client
.getRoomStateWithKey(roomId, PangeaEventTypes.botOptions, "");
}
// invite bot to direct chat
await matrixState.client.setRoomStateWithKey(
roomId, EventTypes.RoomMember, BotName.byEnvironment, {
"membership": Membership.invite.name,
"is_direct": true,
});
await room.addToDirectChat(BotName.byEnvironment);
return null;
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
return;
}
final Room botDMWithLatestActivity = botDMs.reduce((a, b) {

View file

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

View file

@ -26,6 +26,10 @@ class ConstructListModel {
/// All unique lemmas used in the construct events
List<String> get lemmas => constructList.map((e) => e.lemma).toSet().toList();
/// All unique lemmas used in the construct events with non-zero points
List<String> get lemmasWithPoints =>
constructListWithPoints.map((e) => e.lemma).toSet().toList();
/// A map of lemmas to ConstructUses, each of which contains a lemma
/// key = lemmma + constructType.string, value = ConstructUses
void _buildConstructMap() {
@ -72,6 +76,9 @@ class ConstructListModel {
return _constructList!;
}
List<ConstructUses> get constructListWithPoints =>
constructList.where((constructUse) => constructUse.points > 0).toList();
get maxXPPerLemma {
return type != null
? type!.maxXPPerLemma

View file

@ -42,7 +42,7 @@ class ClassDescriptionButton extends StatelessWidget {
? (room.isRoomAdmin
? (room.isSpace
? L10n.of(context)!.classDescriptionDesc
: L10n.of(context)!.chatTopicDesc)
: L10n.of(context)!.setChatDescription)
: L10n.of(context)!.topicNotSet)
: room.topic,
),

View file

@ -2,7 +2,6 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/age_limits.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/pages/p_user_age/p_user_age_view.dart';
import 'package:fluffychat/pangea/utils/p_extension.dart';
import 'package:fluffychat/widgets/fluffy_chat_app.dart';
@ -11,7 +10,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../utils/bot_name.dart';
import '../../utils/error_handler.dart';
class PUserAge extends StatefulWidget {
@ -34,20 +32,7 @@ class PUserAgeController extends State<PUserAge> {
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
if (!(await Matrix.of(context).client.hasBotDM)) {
Matrix.of(context)
.client
.startDirectChat(
BotName.byEnvironment,
enableEncryption: false,
)
.onError(
(error, stackTrace) =>
ErrorHandler.logError(e: error, s: stackTrace),
);
}
});
pangeaController.startChatWithBotIfNotPresent();
}
String? dobValidator() {

View file

@ -41,14 +41,16 @@ class InlineTooltip extends StatelessWidget {
),
const SizedBox(width: 8),
// Text in the middle
Center(
child: Text(
instructionsEnum.body(L10n.of(context)!),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
Flexible(
child: Center(
child: Text(
instructionsEnum.body(L10n.of(context)!),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
),
textAlign: TextAlign.left,
),
textAlign: TextAlign.left,
),
),
// Close button on the right

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

@ -40,23 +40,27 @@ class MissingVoiceButton extends StatelessWidget {
),
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(top: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context)!.voiceNotAvailable,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => launchTTSSettings,
// commenting out as suspecting this is causing an issue
// #freeze-activity
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
child: SizedBox(
width: AppConfig.toolbarMinWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context)!.voiceNotAvailable,
textAlign: TextAlign.center,
),
child: Text(L10n.of(context)!.openVoiceSettings),
),
],
TextButton(
onPressed: () => launchTTSSettings,
// commenting out as suspecting this is causing an issue
// #freeze-activity
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(L10n.of(context)!.openVoiceSettings),
),
],
),
),
);
}

View file

@ -65,33 +65,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!);
@ -106,7 +144,7 @@ class OverlayMessageTextState extends State<OverlayMessageText> {
);
if (mounted) setState(() {});
},
text: tokenPosition.token!.text.content,
text: substring,
style: style.merge(
TextStyle(
backgroundColor: isSelected
@ -119,10 +157,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

@ -16,11 +16,6 @@ class AnalyticsPopup extends StatelessWidget {
super.key,
});
// we just want to show the constructs that have points
List<ConstructUses> get constructs => constructsModel.constructList
.where((constructUse) => constructUse.points > 0)
.toList();
@override
Widget build(BuildContext context) {
return Dialog(
@ -41,29 +36,31 @@ class AnalyticsPopup extends StatelessWidget {
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: constructs.isEmpty
child: constructsModel.constructListWithPoints.isEmpty
? Center(
child: Text(L10n.of(context)!.noDataFound),
)
: ListView.builder(
itemCount: constructs.length,
itemCount: constructsModel.constructListWithPoints.length,
itemBuilder: (context, index) {
return Tooltip(
message:
"${constructs[index].points} / ${constructsModel.maxXPPerLemma}",
"${constructsModel.constructListWithPoints[index].points} / ${constructsModel.maxXPPerLemma}",
child: ListTile(
onTap: () {},
title: Text(
constructsModel.type == ConstructTypeEnum.morph
? getGrammarCopy(
constructsModel
.constructList[index].lemma,
.constructListWithPoints[index].lemma,
context,
)
: constructs[index].lemma,
: constructsModel
.constructListWithPoints[index].lemma,
),
subtitle: LinearProgressIndicator(
value: constructs[index].points /
value: constructsModel
.constructListWithPoints[index].points /
constructsModel.maxXPPerLemma,
minHeight: 20,
borderRadius: const BorderRadius.all(

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:fluffychat/config/themes.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';
@ -37,7 +38,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 +66,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
@ -111,9 +112,9 @@ class LearningProgressIndicatorsState
int? getProgressPoints(ProgressIndicatorEnum indicator) {
switch (indicator) {
case ProgressIndicatorEnum.wordsUsed:
return words?.lemmas.length;
return words?.lemmasWithPoints.length;
case ProgressIndicatorEnum.morphsUsed:
return morphs?.lemmas.length;
return morphs?.lemmasWithPoints.length;
case ProgressIndicatorEnum.level:
return level;
}

View file

@ -24,6 +24,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
Widget build(BuildContext context) {
final discussionChildren = [
TextFormField(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotDiscussionZone_discussionTopicPlaceholder,
@ -43,6 +44,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
),
const SizedBox(height: 12),
TextFormField(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotDiscussionZone_discussionKeywordsPlaceholder,
@ -58,6 +60,7 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
final customChildren = [
TextFormField(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
decoration: InputDecoration(
hintText: L10n.of(context)!
.conversationBotCustomZone_customSystemPromptPlaceholder,

View file

@ -200,27 +200,31 @@ class ConversationBotSettingsDialogState
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 20),
AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity: addBot ? 1.0 : 0.5,
child: ConversationBotSettingsForm(
botOptions: botOptions,
discussionKeywordsController:
discussionKeywordsController,
discussionTopicController: discussionTopicController,
customSystemPromptController:
customSystemPromptController,
enabled: addBot,
onUpdateBotMode: onUpdateChatMode,
onUpdateBotLanguage: onUpdateBotLanguage,
onUpdateBotVoice: onUpdateBotVoice,
onUpdateBotLanguageLevel: onUpdateBotLanguageLevel,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const SizedBox(height: 20),
AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity: addBot ? 1.0 : 0.5,
child: ConversationBotSettingsForm(
botOptions: botOptions,
discussionKeywordsController:
discussionKeywordsController,
discussionTopicController:
discussionTopicController,
customSystemPromptController:
customSystemPromptController,
enabled: addBot,
onUpdateBotMode: onUpdateChatMode,
onUpdateBotLanguage: onUpdateBotLanguage,
onUpdateBotVoice: onUpdateBotVoice,
onUpdateBotLanguageLevel: onUpdateBotLanguageLevel,
),
),
),
],
],
),
),
),
),

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

View file

@ -29,7 +29,9 @@ class TargetTokensController {
_targetTokens = await _initialize(context, pangeaMessageEvent);
await updateTokensWithConstructs(
MatrixState.pangeaController.analytics.analyticsStream.value ?? [],
MatrixState
.pangeaController.analytics.analyticsStream.value?.constructs ??
[],
context,
pangeaMessageEvent,
);
@ -58,9 +60,8 @@ class TargetTokensController {
return _targetTokens = [];
}
return _targetTokens = tokens
.map((token) => token.emptyTokenWithXP)
.toList();
return _targetTokens =
tokens.map((token) => token.emptyTokenWithXP).toList();
}
Future<void> updateTokensWithConstructs(
@ -76,9 +77,8 @@ class TargetTokensController {
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
for (final token in _targetTokens!) {
// we don't need to do this for tokens that don't have saveVocab set to true
if (!token.token.lemma.saveVocab){
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

@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
# Pangea#
publish_to: none
# On version bump also increase the build number for F-Droid
version: 1.23.2+3561
version: 1.23.3+3562
environment:
sdk: ">=3.0.0 <4.0.0"
@ -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