resolve merge conflicts

This commit is contained in:
ggurdin 2025-12-04 15:07:35 -05:00
commit abab3923d9
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
20 changed files with 348 additions and 389 deletions

View file

@ -4990,5 +4990,6 @@
}
},
"pickDifferentActivity": "Pick a different activity",
"messageLanguageMismatchMessage": "Your target language doesn't match this message. Update your target language?"
"messageLanguageMismatchMessage": "Your target language doesn't match this message. Update your target language?",
"blockLemmaConfirmation": "This vocab word will be permanently removed from your analytics"
}

View file

@ -36,7 +36,6 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart';
import 'package:fluffychat/pangea/analytics_misc/message_analytics_feedback.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart';
import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart';
@ -471,7 +470,7 @@ class ChatController extends State<ChatPageWithRoom>
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
OverlayUtil.showPointsGained(update.targetID!, update.points, context);
}
}
@ -2107,14 +2106,11 @@ class ChatController extends State<ChatPageWithRoom>
];
_showAnalyticsFeedback(constructs, eventId);
pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: eventId,
targetID: eventId,
roomId: room.id,
constructs: constructs,
),
pangeaController.putAnalytics.addAnalytics(
constructs,
eventId: eventId,
targetId: eventId,
roomId: room.id,
);
}
}
@ -2161,13 +2157,11 @@ class ChatController extends State<ChatPageWithRoom>
if (constructs.isEmpty) return;
_showAnalyticsFeedback(constructs, eventId);
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: eventId,
targetID: eventId,
roomId: room.id,
constructs: constructs,
),
MatrixState.pangeaController.putAnalytics.addAnalytics(
constructs,
eventId: eventId,
targetId: eventId,
roomId: room.id,
);
} catch (e, s) {
ErrorHandler.logError(
@ -2312,6 +2306,36 @@ class ChatController extends State<ChatPageWithRoom>
);
}
}
Future<void> onLeave() async {
final parentSpaceId = room.courseParent?.id;
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).leaveRoomDescription,
okLabel: L10n.of(context).leave,
cancelLabel: L10n.of(context).cancel,
isDestructive: true,
);
if (confirmed != OkCancelResult.ok) return;
final result = await showFutureLoadingDialog(
context: context,
future: widget.room.leave,
);
if (result.isError) return;
final r = Matrix.of(context).client.getRoomById(widget.room.id);
if (r != null && r.membership != Membership.leave) {
await Matrix.of(context).client.waitForRoomInSync(
widget.room.id,
leave: true,
);
}
context.go(
parentSpaceId != null ? '/rooms/spaces/$parentSpaceId' : '/rooms',
);
}
// Pangea#
late final ValueNotifier<bool> _displayChatDetailsColumn;

View file

@ -147,7 +147,10 @@ class ChatView extends StatelessWidget {
if (controller.room.showActivityChatUI) {
return [
ActivityMenuButton(controller: controller),
ActivitySessionPopupMenu(controller.room),
ActivitySessionPopupMenu(
controller.room,
onLeave: controller.onLeave,
),
];
}

View file

@ -6,15 +6,14 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_details/chat_download_provider.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
enum ActivityPopupMenuActions { invite, leave, download }
class ActivitySessionPopupMenu extends StatefulWidget {
final Room room;
final VoidCallback onLeave;
const ActivitySessionPopupMenu(this.room, {super.key});
const ActivitySessionPopupMenu(this.room, {required this.onLeave, super.key});
@override
ActivitySessionPopupMenuState createState() =>
@ -30,28 +29,7 @@ class ActivitySessionPopupMenuState extends State<ActivitySessionPopupMenu>
onSelected: (choice) async {
switch (choice) {
case ActivityPopupMenuActions.leave:
final parentSpaceId = widget.room.courseParent?.id;
final router = GoRouter.of(context);
final confirmed = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).leaveRoomDescription,
okLabel: L10n.of(context).leave,
cancelLabel: L10n.of(context).cancel,
isDestructive: true,
);
if (confirmed != OkCancelResult.ok) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => widget.room.leave(),
);
if (result.error == null) {
router.go(
parentSpaceId != null
? '/rooms/spaces/$parentSpaceId'
: '/rooms',
);
}
widget.onLeave();
break;
case ActivityPopupMenuActions.invite:
context.go(

View file

@ -135,6 +135,14 @@ class ConstructListModel {
level = calculateLevelWithXp(totalXP);
}
void deleteConstruct(ConstructIdentifier constructId, int offset) {
_uses.removeWhere((use) => use.identifier == constructId);
_constructMap.removeWhere(
(key, value) => value.id == constructId,
);
updateConstructs([], offset);
}
List<ConstructUses> constructList({ConstructTypeEnum? type}) => _constructList
.where(
(constructUse) => type == null || constructUse.constructType == type,

View file

@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
@ -84,6 +85,12 @@ class GetAnalyticsController extends BaseController {
.putAnalytics.analyticsUpdateStream.stream
.listen(_onAnalyticsUpdate);
_pangeaController.putAnalytics.savedActivitiesNotifier
.addListener(_onActivityAnalyticsUpdate);
_pangeaController.putAnalytics.blockedConstructsNotifier
.addListener(_onBlockedConstructsUpdate);
// When a newly-joined space comes through in a sync
// update, add the analytics rooms to the space
_joinSpaceSubscription ??= _client.onSync.stream
@ -95,6 +102,20 @@ class GetAnalyticsController extends BaseController {
final offset =
_pangeaController.userController.analyticsProfile?.xpOffset ?? 0;
final allUses = [
...(_getConstructsLocal() ?? []),
..._locallyCachedConstructs,
];
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
final blockedLemmas = analyticsRoom?.analyticsSettings?.blockedConstructs;
if (blockedLemmas != null && blockedLemmas.isNotEmpty) {
allUses.removeWhere(
(use) => blockedLemmas.contains(use.identifier),
);
}
constructListModel.updateConstructs(
[
...(_getConstructsLocal() ?? []),
@ -109,11 +130,7 @@ class GetAnalyticsController extends BaseController {
data: {},
);
} finally {
_updateAnalyticsStream(
type: AnalyticsUpdateType.init,
points: 0,
newConstructs: [],
);
_updateAnalyticsStream(AnalyticsStreamUpdate());
if (!initCompleter.isCompleted) initCompleter.complete();
_initializing = false;
}
@ -128,23 +145,16 @@ class GetAnalyticsController extends BaseController {
_joinSpaceSubscription?.cancel();
_joinSpaceSubscription = null;
initCompleter = Completer<void>();
_pangeaController.putAnalytics.savedActivitiesNotifier
.removeListener(_onActivityAnalyticsUpdate);
_pangeaController.putAnalytics.blockedConstructsNotifier
.removeListener(_onBlockedConstructsUpdate);
_cache.clear();
// perMessage.dispose();
}
Future<void> _onAnalyticsUpdate(
AnalyticsUpdate analyticsUpdate,
) async {
final validTypes = [AnalyticsUpdateType.local, AnalyticsUpdateType.server];
if (!validTypes.contains(analyticsUpdate.type)) {
_updateAnalyticsStream(
type: analyticsUpdate.type,
points: 0,
newConstructs: [],
);
return;
}
if (analyticsUpdate.isLogout) return;
final oldLevel = constructListModel.level;
@ -159,9 +169,6 @@ class GetAnalyticsController extends BaseController {
)
.toSet();
final prevUnlockedVocab =
constructListModel.unlockedLemmas(ConstructTypeEnum.vocab).toSet();
constructListModel.updateConstructs(analyticsUpdate.newConstructs, offset);
final newUnlockedMorphs = constructListModel
@ -172,11 +179,6 @@ class GetAnalyticsController extends BaseController {
.toSet()
.difference(prevUnlockedMorphs);
final newUnlockedVocab = constructListModel
.unlockedLemmas(ConstructTypeEnum.vocab)
.toSet()
.difference(prevUnlockedVocab);
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
await _getConstructs(forceUpdate: true);
}
@ -192,13 +194,13 @@ class GetAnalyticsController extends BaseController {
_onUnlockMorphLemmas(newUnlockedMorphs);
}
_updateAnalyticsStream(
type: analyticsUpdate.type,
points: analyticsUpdate.newConstructs.fold<int>(
0,
(previousValue, element) => previousValue + element.xp,
AnalyticsStreamUpdate(
points: analyticsUpdate.newConstructs.fold<int>(
0,
(previousValue, element) => previousValue + element.xp,
),
targetID: analyticsUpdate.targetID,
),
targetID: analyticsUpdate.targetID,
newConstructs: [...newUnlockedMorphs, ...newUnlockedVocab],
);
// Update public profile each time that new analytics are added.
// If the level hasn't changed, this will not send an update to the server.
@ -209,20 +211,8 @@ class GetAnalyticsController extends BaseController {
);
}
void _updateAnalyticsStream({
required AnalyticsUpdateType type,
required int points,
required List<ConstructIdentifier> newConstructs,
String? targetID,
}) =>
analyticsStream.add(
AnalyticsStreamUpdate(
type: type,
points: points,
newConstructs: newConstructs,
targetID: targetID,
),
);
void _updateAnalyticsStream(AnalyticsStreamUpdate update) =>
analyticsStream.add(update);
void _onLevelUp(final int lowerLevel, final int upperLevel) {
setState({
@ -253,6 +243,21 @@ class GetAnalyticsController extends BaseController {
setState({'unlocked_constructs': filtered});
}
void _onActivityAnalyticsUpdate() =>
_updateAnalyticsStream(AnalyticsStreamUpdate());
void _onBlockedConstructsUpdate() {
final constructId =
_pangeaController.putAnalytics.blockedConstructsNotifier.value;
if (constructId == null) return;
constructListModel.deleteConstruct(
constructId,
_pangeaController.userController.analyticsProfile?.xpOffset ?? 0,
);
_updateAnalyticsStream(AnalyticsStreamUpdate());
}
/// A local cache of eventIds and construct uses for messages sent since the last update.
/// It's a map of eventIDs to a list of OneConstructUses. Not just a list of OneConstructUses
/// because, with practice activity constructs, we might need to add to the list for a given
@ -286,8 +291,7 @@ class GetAnalyticsController extends BaseController {
return formattedCache;
} catch (err) {
// if something goes wrong while trying to format the local data, clear it
_pangeaController.putAnalytics
.clearMessagesSinceUpdate(clearDrafts: true);
clearMessagesCache();
return {};
}
} catch (exception, stackTrace) {
@ -471,19 +475,6 @@ class GetAnalyticsController extends BaseController {
return newConstructCount;
}
// Future<GenerateConstructSummaryResult?>
// _generateLevelUpAnalyticsAndSaveToStateEvent(
// final int lowerLevel,
// final int upperLevel,
// ) async {
// // generate level up analytics as a construct summary
// ConstructSummary summary;
// try {
// final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
// final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
// int diffXP = maxXP - minXP;
// if (diffXP < 0) diffXP = 0;
ConstructSummary? getConstructSummaryFromStateEvent() {
try {
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
@ -649,15 +640,11 @@ class AnalyticsCacheEntry {
}
class AnalyticsStreamUpdate {
final AnalyticsUpdateType type;
final int points;
final List<ConstructIdentifier> newConstructs;
final String? targetID;
AnalyticsStreamUpdate({
required this.type,
required this.points,
required this.newConstructs,
this.points = 0,
this.targetID,
});
}

View file

@ -1,6 +1,5 @@
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -28,27 +27,27 @@ mixin LemmaEmojiSetter {
String? roomId,
String? targetId,
}) {
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: eventId,
roomId: roomId,
targetID: targetId,
constructs: [
OneConstructUse(
useType: ConstructUseTypeEnum.em,
lemma: constructId.lemma,
constructType: constructId.type,
metadata: ConstructUseMetaData(
roomId: roomId,
timeStamp: DateTime.now(),
eventId: eventId,
),
category: constructId.category,
form: constructId.lemma,
xp: ConstructUseTypeEnum.em.pointValue,
),
],
final constructs = [
OneConstructUse(
useType: ConstructUseTypeEnum.em,
lemma: constructId.lemma,
constructType: constructId.type,
metadata: ConstructUseMetaData(
roomId: roomId,
timeStamp: DateTime.now(),
eventId: eventId,
),
category: constructId.category,
form: constructId.lemma,
xp: ConstructUseTypeEnum.em.pointValue,
),
];
MatrixState.pangeaController.putAnalytics.addAnalytics(
constructs,
eventId: eventId,
roomId: roomId,
targetId: targetId,
);
}
}

View file

@ -5,27 +5,31 @@ import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_model.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/languages/language_model.dart';
import 'package:fluffychat/pangea/user/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum AnalyticsUpdateType { server, local, activities, init }
enum AnalyticsUpdateType { server, local }
/// handles the processing of analytics for
/// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
class PutAnalyticsController extends BaseController<AnalyticsStream> {
class PutAnalyticsController {
late PangeaController _pangeaController;
StreamController<AnalyticsUpdate> analyticsUpdateStream =
StreamController.broadcast();
StreamSubscription<AnalyticsStream>? _analyticsStream;
ValueNotifier<List<String>> savedActivitiesNotifier = ValueNotifier([]);
ValueNotifier<ConstructIdentifier?> blockedConstructsNotifier =
ValueNotifier(null);
StreamSubscription? _languageStream;
Timer? _updateTimer;
@ -53,31 +57,19 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
}
void initialize() {
// Listen for calls to setState on the analytics stream
// and update the analytics room if necessary
_analyticsStream ??=
stateStream.listen((data) => _onNewAnalyticsData(data));
// Listen for changes to the user's language settings
_languageStream ??=
_pangeaController.userController.languageStream.stream.listen((update) {
_onUpdateLanguages(update.prevTargetLang);
});
_languageStream ??= _pangeaController.userController.languageStream.stream
.listen(_onUpdateLanguages);
_refreshAnalyticsIfOutdated();
}
/// Reset analytics last updated time to null.
@override
void dispose() {
_updateTimer?.cancel();
lastUpdated = null;
lastUpdatedCompleter = Completer<DateTime?>();
_analyticsStream?.cancel();
_analyticsStream = null;
_languageStream?.cancel();
_languageStream = null;
clearMessagesSinceUpdate();
MatrixState.pangeaController.getAnalytics.clearMessagesCache();
}
/// If analytics haven't been updated in the last day, update them
@ -112,137 +104,18 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
/// Given new construct uses, format and cache
/// the data locally and reset the update timer
/// Decide whether to update the analytics room
void _onNewAnalyticsData(AnalyticsStream data) {
final String? eventID = data.eventId;
final String? roomID = data.roomId;
final List<OneConstructUse> constructs = [];
// if (roomID != null) {
// constructs = _getDraftUses(roomID);
// }
constructs.addAll(data.constructs);
if (kDebugMode) {
for (final use in constructs) {
debugPrint(
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.xp}",
);
}
}
void addAnalytics(
List<OneConstructUse> constructs, {
String? eventId,
String? roomId,
String? targetId,
}) {
final level = _pangeaController.getAnalytics.constructListModel.level;
_addLocalMessage(eventID, constructs).then(
(_) {
if (roomID != null) _clearDraftUses(roomID);
_decideWhetherToUpdateAnalyticsRoom(
level,
data.targetID,
data.constructs,
);
},
_addLocalMessage(eventId, constructs).then(
(_) => _sendAnalytics(level, targetId, constructs),
);
}
Future<void> _onUpdateLanguages(LanguageModel? previousL2) async {
await sendLocalAnalyticsToAnalyticsRoom(
l2Override: previousL2,
);
_pangeaController.resetAnalytics().then((_) {
final level = _pangeaController.getAnalytics.constructListModel.level;
_pangeaController.userController.updateAnalyticsProfile(level: level);
});
}
// void addDraftUses(
// List<PangeaToken> tokens,
// String roomID,
// ConstructUseTypeEnum useType, {
// String? targetID,
// }) {
// final metadata = ConstructUseMetaData(
// roomId: roomID,
// timeStamp: DateTime.now(),
// );
// // we only save those with saveVocab == true
// final tokensToSave =
// tokens.where((token) => token.lemma.saveVocab).toList();
// // get all our vocab constructs
// final uses = tokensToSave
// .map(
// (token) => OneConstructUse(
// useType: useType,
// lemma: token.lemma.text,
// form: token.text.content,
// constructType: ConstructTypeEnum.vocab,
// metadata: metadata,
// category: token.pos,
// ),
// )
// .toList();
// // get all our grammar constructs
// for (final token in tokensToSave) {
// uses.add(
// OneConstructUse(
// useType: useType,
// lemma: token.pos,
// form: token.text.content,
// category: "POS",
// constructType: ConstructTypeEnum.morph,
// metadata: metadata,
// ),
// );
// for (final entry in token.morph.entries) {
// uses.add(
// OneConstructUse(
// useType: useType,
// lemma: entry.value,
// form: token.text.content,
// category: entry.key,
// constructType: ConstructTypeEnum.morph,
// metadata: metadata,
// ),
// );
// }
// }
// if (kDebugMode) {
// for (final use in uses) {
// debugPrint(
// "Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
// );
// }
// }
// final level = _pangeaController.getAnalytics.constructListModel.level;
// // the list 'uses' gets altered in the _addLocalMessage method,
// // so copy it here to that the list of new uses is accurate
// final List<OneConstructUse> newUses = List.from(uses);
// _addLocalMessage('draft$roomID', uses).then(
// (_) => _decideWhetherToUpdateAnalyticsRoom(
// level,
// targetID,
// newUses,
// ),
// );
// }
// List<OneConstructUse> _getDraftUses(String roomID) {
// final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
// return currentCache['draft$roomID'] ?? [];
// }
void _clearDraftUses(String roomID) {
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
currentCache.remove('draft$roomID');
_setMessagesSinceUpdate(currentCache);
}
/// Add a list of construct uses for a new message to the local
/// cache of recently sent messages
Future<void> _addLocalMessage(
@ -255,7 +128,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
// if this is not a draft message, add the eventId to the metadata
// if it's missing (it will be missing for draft constructs)
if (cacheKey != null && !cacheKey.startsWith('draft')) {
if (cacheKey != null) {
constructs = constructs.map((construct) {
if (construct.metadata.eventId != null) return construct;
construct.metadata.eventId = cacheKey;
@ -283,7 +156,7 @@ class PutAnalyticsController 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(
void _sendAnalytics(
int prevLevel,
String? targetID,
List<OneConstructUse> newConstructs,
@ -311,25 +184,14 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
);
}
/// Clears the local cache of recently sent constructs. Called before updating analytics
void clearMessagesSinceUpdate({clearDrafts = false}) {
if (clearDrafts) {
MatrixState.pangeaController.getAnalytics.clearMessagesCache();
return;
}
final localCache = _pangeaController.getAnalytics.messagesSinceUpdate;
final draftKeys = localCache.keys.where((key) => key.startsWith('draft'));
if (draftKeys.isEmpty) {
MatrixState.pangeaController.getAnalytics.clearMessagesCache();
return;
}
final Map<String, List<OneConstructUse>> newCache = {};
for (final key in draftKeys) {
newCache[key] = localCache[key]!;
}
_setMessagesSinceUpdate(newCache);
Future<void> _onUpdateLanguages(LanguageUpdate update) async {
await sendLocalAnalyticsToAnalyticsRoom(
l2Override: update.prevTargetLang,
);
_pangeaController.resetAnalytics().then((_) {
final level = _pangeaController.getAnalytics.constructListModel.level;
_pangeaController.userController.updateAnalyticsProfile(level: level);
});
}
/// Save the local cache of recently sent constructs to the local storage
@ -369,7 +231,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
_updateCompleter = Completer<void>();
try {
await _updateAnalytics(l2Override: l2Override);
clearMessagesSinceUpdate();
MatrixState.pangeaController.getAnalytics.clearMessagesCache();
lastUpdated = DateTime.now();
analyticsUpdateStream.add(
@ -425,48 +287,34 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
);
if (analyticsRoom == null) return;
await analyticsRoom.addActivityRoomId(roomId);
analyticsUpdateStream.add(
AnalyticsUpdate(
AnalyticsUpdateType.activities,
[],
),
);
savedActivitiesNotifier.value = analyticsRoom.activityRoomIds;
}
Future<void> removeActivityAnalytics(String roomId) async {
if (_client.userID == null) return;
Future<void> blockConstruct(ConstructIdentifier constructId) async {
if (_pangeaController.matrixState.client.userID == null) return;
if (_pangeaController.userController.userL2 == null) return;
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(
_pangeaController.userController.userL2!,
);
if (analyticsRoom == null) return;
await analyticsRoom.removeActivityRoomId(roomId);
analyticsUpdateStream.add(
AnalyticsUpdate(
AnalyticsUpdateType.activities,
[],
),
final current = analyticsRoom.analyticsSettings ??
const AnalyticsSettingsModel(blockedConstructs: {});
final blockedConstructs = current.blockedConstructs;
final updated = current.copyWith(
blockedConstructs: {
...blockedConstructs,
constructId,
},
);
await analyticsRoom.setAnalyticsSettings(updated);
blockedConstructsNotifier.value = constructId;
}
}
class AnalyticsStream {
final String? eventId;
final String? roomId;
final String? targetID;
final List<OneConstructUse> constructs;
AnalyticsStream({
required this.eventId,
required this.roomId,
required this.constructs,
this.targetID,
});
}
class AnalyticsUpdate {
final AnalyticsUpdateType type;
final List<OneConstructUse> newConstructs;

View file

@ -5,15 +5,17 @@ import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
import 'package:fluffychat/pangea/analytics_page/analytics_page_constants.dart';
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart';
import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class AnalyticsPage extends StatelessWidget {
@ -28,18 +30,48 @@ class AnalyticsPage extends StatelessWidget {
this.isSidebar = false,
});
Future<void> _blockLemma(BuildContext context) async {
final resp = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).blockLemmaConfirmation,
isDestructive: true,
);
if (resp != OkCancelResult.ok) return;
final res = await showFutureLoadingDialog(
context: context,
future: () =>
MatrixState.pangeaController.putAnalytics.blockConstruct(construct!),
);
if (!res.isError) {
context.go("/rooms/analytics/${ConstructTypeEnum.vocab.name}");
}
}
@override
Widget build(BuildContext context) {
final analyticsRoomId = GoRouterState.of(context).pathParameters['roomid'];
return Scaffold(
appBar: construct != null ? AppBar() : null,
appBar: construct != null
? AppBar(
actions: indicator == ProgressIndicatorEnum.wordsUsed
? [
IconButton(
icon: const Icon(Icons.delete_forever_outlined),
color: Theme.of(context).colorScheme.error,
tooltip: L10n.of(context).delete,
onPressed: () => _blockLemma(context),
),
]
: null,
)
: null,
body: SafeArea(
child: StreamBuilder(
stream: MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.where(
(u) => u.type == AnalyticsUpdateType.init,
),
child: FutureBuilder(
future:
MatrixState.pangeaController.getAnalytics.initCompleter.future,
builder: (context, snapshot) {
return Padding(
padding: const EdgeInsetsGeometry.all(16.0),

View file

@ -0,0 +1,23 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_model.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
extension AnalyticsSettingsRoomExtension on Room {
AnalyticsSettingsModel? get analyticsSettings {
final event = getState(PangeaEventTypes.analyticsSettings);
if (event == null) return null;
return AnalyticsSettingsModel.fromJson(event.content);
}
Future<void> setAnalyticsSettings(
AnalyticsSettingsModel settings,
) async {
await client.setRoomStateWithKey(
id,
PangeaEventTypes.analyticsSettings,
"",
settings.toJson(),
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
class AnalyticsSettingsModel {
final Set<ConstructIdentifier> blockedConstructs;
const AnalyticsSettingsModel({
required this.blockedConstructs,
});
AnalyticsSettingsModel copyWith({
Set<ConstructIdentifier>? blockedConstructs,
}) {
return AnalyticsSettingsModel(
blockedConstructs: blockedConstructs ?? this.blockedConstructs,
);
}
factory AnalyticsSettingsModel.fromJson(Map<String, dynamic> json) {
final blockedConstructs = <ConstructIdentifier>{};
if (json['blocked_constructs'] != null) {
final lemmas = json['blocked_constructs'] as List<dynamic>;
for (final lemma in lemmas) {
blockedConstructs.add(ConstructIdentifier.fromJson(lemma));
}
}
return AnalyticsSettingsModel(
blockedConstructs: blockedConstructs,
);
}
Map<String, dynamic> toJson() {
return {
'blocked_constructs': blockedConstructs.map((c) => c.toJson()).toList(),
};
}
}

View file

@ -51,11 +51,11 @@ class LearningProgressIndicatorsState
// if getAnalytics has already finished initializing,
// the data is loaded and should be displayed.
if (MatrixState.pangeaController.getAnalytics.initCompleter.isCompleted) {
updateData(null);
updateData();
}
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen(updateData);
.listen((_) => updateData());
// rebuild when target language changes
_languageSubscription = MatrixState
@ -71,10 +71,11 @@ class LearningProgressIndicatorsState
_analyticsSubscription = null;
_languageSubscription?.cancel();
_languageSubscription = null;
super.dispose();
}
void updateData(AnalyticsStreamUpdate? _) {
void updateData() {
if (_loading) _loading = false;
if (mounted) setState(() {});
}

View file

@ -6,7 +6,10 @@ import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/chat_settings/utils/bot_client_extension.dart';
@ -127,6 +130,19 @@ class PangeaController {
if (exclude.contains(key)) continue;
futures.add(GetStorage(key).erase());
}
if (AppConfig.showedActivityMenu) {
futures.add(
SharedPreferences.getInstance().then((prefs) async {
AppConfig.showedActivityMenu = false;
prefs.setBool(
SettingKeys.showedActivityMenu,
AppConfig.showedActivityMenu,
);
}),
);
}
await Future.wait(futures);
}

View file

@ -289,6 +289,7 @@ class OverlayUtil {
static void showPointsGained(
String targetId,
int points,
BuildContext context,
) {
showOverlay(
@ -297,7 +298,7 @@ class OverlayUtil {
targetAnchor: Alignment.bottomCenter,
context: context,
child: PointsGainedAnimation(
points: 2,
points: points,
targetID: targetId,
),
transformTargetId: targetId,

View file

@ -54,4 +54,6 @@ class PangeaEventTypes {
static const courseUser = "p.course_user";
static const teacherMode = "pangea.teacher_mode";
static const courseChatList = "pangea.course_chat_list";
static const analyticsSettings = "pangea.analytics_settings";
}

View file

@ -81,7 +81,7 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
OverlayUtil.showPointsGained(update.targetID!, update.points, context);
}
}

View file

@ -77,17 +77,19 @@ class PracticeSelectionRepo {
static PracticeSelection? _getCached(
String eventId,
) {
for (final String key in _storage.getKeys()) {
try {
try {
final keys = List.from(_storage.getKeys());
for (final String key in keys) {
final cacheEntry = _PracticeSelectionCacheEntry.fromJson(
_storage.read(key),
);
if (cacheEntry.isExpired) {
_storage.remove(key);
}
} catch (e) {
_storage.remove(key);
}
} catch (e) {
_storage.erase();
return null;
}
final entry = _storage.read(eventId);

View file

@ -13,7 +13,6 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
@ -193,27 +192,26 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {});
if (selectedToken != null && isNewToken(selectedToken!)) {
final token = selectedToken!;
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event.eventId,
roomId: event.room.id,
constructs: [
OneConstructUse(
useType: ConstructUseTypeEnum.click,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: event.room.id,
timeStamp: DateTime.now(),
eventId: event.eventId,
),
category: token.pos,
form: token.text.content,
xp: ConstructUseTypeEnum.click.pointValue,
),
],
targetID: "word-zoom-card-${token.text.uniqueKey}",
final constructs = [
OneConstructUse(
useType: ConstructUseTypeEnum.click,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: event.room.id,
timeStamp: DateTime.now(),
eventId: event.eventId,
),
category: token.pos,
form: token.text.content,
xp: ConstructUseTypeEnum.click.pointValue,
),
];
MatrixState.pangeaController.putAnalytics.addAnalytics(
constructs,
eventId: event.eventId,
roomId: event.room.id,
targetId: "word-zoom-card-${token.text.uniqueKey}",
);
}
}

View file

@ -6,7 +6,6 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
@ -125,28 +124,28 @@ class PracticeController with ChangeNotifier {
final constructUseType = _activity!.practiceTarget.record.responses.last
.useType(_activity!.activityType);
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: pangeaMessageEvent.eventId,
roomId: pangeaMessageEvent.room.id,
constructs: [
OneConstructUse(
useType: constructUseType,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: pangeaMessageEvent.room.id,
timeStamp: DateTime.now(),
eventId: pangeaMessageEvent.eventId,
),
category: token.pos,
// in the case of a wrong answer, the cId doesn't match the token
form: token.text.content,
xp: constructUseType.pointValue,
),
],
targetID: targetId,
final constructs = [
OneConstructUse(
useType: constructUseType,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
roomId: pangeaMessageEvent.room.id,
timeStamp: DateTime.now(),
eventId: pangeaMessageEvent.eventId,
),
category: token.pos,
// in the case of a wrong answer, the cId doesn't match the token
form: token.text.content,
xp: constructUseType.pointValue,
),
];
MatrixState.pangeaController.putAnalytics.addAnalytics(
constructs,
eventId: pangeaMessageEvent.eventId,
roomId: pangeaMessageEvent.room.id,
targetId: targetId,
);
}

View file

@ -152,6 +152,7 @@ abstract class ClientManager {
PangeaEventTypes.courseUser,
PangeaEventTypes.teacherMode,
PangeaEventTypes.courseChatList,
PangeaEventTypes.analyticsSettings,
// Pangea#
},
logLevel: kReleaseMode ? Level.warning : Level.verbose,