feat: add language indicators to analytics database entries (#5692)
* feat: add language indicators to analytics database entries * better handling for database reinit * don't clear database is last update not set
This commit is contained in:
parent
1de440156c
commit
3537cd5fd4
30 changed files with 524 additions and 198 deletions
|
|
@ -2227,8 +2227,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
),
|
||||
];
|
||||
|
||||
_showAnalyticsFeedback(constructs, eventId);
|
||||
addAnalytics(constructs, eventId);
|
||||
final langCode = originalSent.langCode.split('-').first;
|
||||
_showAnalyticsFeedback(constructs, eventId, langCode);
|
||||
addAnalytics(constructs, eventId, langCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2241,10 +2242,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
final constructs = stt.constructs(roomId, eventId);
|
||||
if (constructs.isEmpty) return;
|
||||
|
||||
_showAnalyticsFeedback(constructs, eventId);
|
||||
Matrix.of(
|
||||
context,
|
||||
).analyticsDataService.updateService.addAnalytics(eventId, constructs);
|
||||
final langCode = stt.langCode.split('-').first;
|
||||
_showAnalyticsFeedback(constructs, eventId, langCode);
|
||||
Matrix.of(context).analyticsDataService.updateService.addAnalytics(
|
||||
eventId,
|
||||
constructs,
|
||||
langCode,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
@ -2398,16 +2402,19 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
Future<void> _showAnalyticsFeedback(
|
||||
List<OneConstructUse> constructs,
|
||||
String eventId,
|
||||
String language,
|
||||
) async {
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
final newGrammarConstructs = await analyticsService.getNewConstructCount(
|
||||
constructs,
|
||||
ConstructTypeEnum.morph,
|
||||
language,
|
||||
);
|
||||
|
||||
final newVocabConstructs = await analyticsService.getNewConstructCount(
|
||||
constructs,
|
||||
ConstructTypeEnum.vocab,
|
||||
language,
|
||||
);
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_session_analytics_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -103,6 +104,10 @@ class ActivityChatController {
|
|||
Future<ActivitySummaryAnalyticsModel> getActivityAnalytics() async {
|
||||
final cached = ActivitySessionAnalyticsRepo.get(room.id);
|
||||
final analytics = cached?.analytics ?? ActivitySummaryAnalyticsModel();
|
||||
final activityLang = room.activityPlan?.req.targetLanguage;
|
||||
if (activityLang == null) {
|
||||
return analytics;
|
||||
}
|
||||
|
||||
DateTime? timestamp = room.creationTimestamp;
|
||||
if (cached != null) {
|
||||
|
|
@ -114,6 +119,7 @@ class ActivityChatController {
|
|||
MatrixState.pangeaController.matrixState.analyticsDataService;
|
||||
|
||||
uses = await analyticsService.getUses(
|
||||
activityLang.split('-').first,
|
||||
since: timestamp ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
roomId: room.id,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ class _VocabChipsState extends State<_VocabChips> with TokenRenderingMixin {
|
|||
"activity_tokens",
|
||||
widget.targetId,
|
||||
token,
|
||||
widget.langCode.split('-').first,
|
||||
Matrix.of(context).analyticsDataService,
|
||||
).then((_) {
|
||||
if (mounted) setState(() {});
|
||||
|
|
|
|||
|
|
@ -121,13 +121,31 @@ class AnalyticsDataService {
|
|||
}
|
||||
|
||||
_invalidateCaches();
|
||||
final l2 = MatrixState.pangeaController.userController.userL2;
|
||||
final analyticsUserId = await _analyticsClientGetter.database.getUserID();
|
||||
final lastUpdated = await _analyticsClientGetter.database
|
||||
.getLastUpdated();
|
||||
final analyticsLanguage = await _analyticsClientGetter.database
|
||||
.getCurrentLanguage();
|
||||
|
||||
if (analyticsUserId != client.userID || lastUpdated == null) {
|
||||
await _clearDatabase();
|
||||
if (analyticsUserId != client.userID || analyticsLanguage == null) {
|
||||
// If current language not set, analytics database needs be updated to include language flag, so clear it.
|
||||
// If user ID doesn't match, this means that a different user has logged in since the last time the database was initialized,
|
||||
// so clear it to avoid showing another user's analytics.
|
||||
_clear();
|
||||
await _analyticsClientGetter.database.clear();
|
||||
await _analyticsClientGetter.database.updateUserID(client.userID!);
|
||||
if (l2 != null) {
|
||||
await _analyticsClientGetter.database.updateCurrentLanguage(
|
||||
l2.langCodeShort,
|
||||
);
|
||||
}
|
||||
} else if (l2 != null && analyticsLanguage != l2.langCodeShort) {
|
||||
// If the current language doesn't match the language in the database, this means that
|
||||
// the user has switched their L2 since the last time the database was initialized.
|
||||
// Clear local cache / merge table data.
|
||||
_clear();
|
||||
await _analyticsClientGetter.database.updateCurrentLanguage(
|
||||
l2.langCodeShort,
|
||||
);
|
||||
}
|
||||
|
||||
_syncController?.dispose();
|
||||
|
|
@ -136,21 +154,27 @@ class AnalyticsDataService {
|
|||
dataService: this,
|
||||
);
|
||||
|
||||
await _syncController!.bulkUpdate();
|
||||
if (l2 != null) {
|
||||
await _syncController!.bulkUpdate(l2.langCodeShort);
|
||||
}
|
||||
|
||||
final resp = await client.getUserProfile(client.userID!);
|
||||
final analyticsProfile = AnalyticsProfileModel.fromJson(
|
||||
resp.additionalProperties,
|
||||
);
|
||||
|
||||
final l2 = MatrixState.pangeaController.userController.userL2;
|
||||
if (l2 != null) {
|
||||
await updateXPOffset(analyticsProfile.xpOffsetByLanguage(l2) ?? 0);
|
||||
await updateXPOffset(
|
||||
analyticsProfile.xpOffsetByLanguage(l2) ?? 0,
|
||||
l2.langCodeShort,
|
||||
);
|
||||
}
|
||||
|
||||
_syncController!.start();
|
||||
|
||||
await _initMergeTable();
|
||||
if (l2 != null) {
|
||||
await _initMergeTable(l2.langCodeShort);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logs().e("Error initializing analytics: $e, $s");
|
||||
} finally {
|
||||
|
|
@ -161,12 +185,14 @@ class AnalyticsDataService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _initMergeTable() async {
|
||||
Future<void> _initMergeTable(String language) async {
|
||||
final vocab = await _analyticsClientGetter.database.getAggregatedConstructs(
|
||||
ConstructTypeEnum.vocab,
|
||||
language,
|
||||
);
|
||||
final morph = await _analyticsClientGetter.database.getAggregatedConstructs(
|
||||
ConstructTypeEnum.morph,
|
||||
language,
|
||||
);
|
||||
|
||||
final blocked = blockedConstructs;
|
||||
|
|
@ -177,12 +203,11 @@ class AnalyticsDataService {
|
|||
Future<void> reinitialize() async {
|
||||
Logs().i("Reinitializing analytics database.");
|
||||
initCompleter = Completer<void>();
|
||||
await _clearDatabase();
|
||||
_clear();
|
||||
await _initDatabase(_analyticsClientGetter.client);
|
||||
}
|
||||
|
||||
Future<void> _clearDatabase() async {
|
||||
await _analyticsClient?.database.clear();
|
||||
void _clear() {
|
||||
_invalidateCaches();
|
||||
_mergeTable.clear();
|
||||
}
|
||||
|
|
@ -190,8 +215,7 @@ class AnalyticsDataService {
|
|||
Future<void> _closeDatabase() async {
|
||||
await _analyticsClient?.database.delete();
|
||||
_analyticsClient = null;
|
||||
_invalidateCaches();
|
||||
_mergeTable.clear();
|
||||
_clear();
|
||||
}
|
||||
|
||||
Future<void> _ensureInitialized() =>
|
||||
|
|
@ -217,23 +241,24 @@ class AnalyticsDataService {
|
|||
|
||||
DerivedAnalyticsDataModel? get cachedDerivedData => _cachedDerivedStats;
|
||||
|
||||
Future<DerivedAnalyticsDataModel> get derivedData async {
|
||||
Future<DerivedAnalyticsDataModel> derivedData(String language) async {
|
||||
await _ensureInitialized();
|
||||
|
||||
if (_cachedDerivedStats == null || _derivedCacheVersion != _cacheVersion) {
|
||||
_cachedDerivedStats = await _analyticsClientGetter.database
|
||||
.getDerivedStats();
|
||||
.getDerivedStats(language);
|
||||
_derivedCacheVersion = _cacheVersion;
|
||||
}
|
||||
|
||||
return _cachedDerivedStats!;
|
||||
}
|
||||
|
||||
Future<DateTime?> getLastUpdatedAnalytics() async {
|
||||
return _analyticsClientGetter.database.getLastEventTimestamp();
|
||||
Future<DateTime?> getLastUpdatedAnalytics(String language) async {
|
||||
return _analyticsClientGetter.database.getLastEventTimestamp(language);
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getUses({
|
||||
Future<List<OneConstructUse>> getUses(
|
||||
String language, {
|
||||
int? count,
|
||||
String? roomId,
|
||||
DateTime? since,
|
||||
|
|
@ -242,6 +267,7 @@ class AnalyticsDataService {
|
|||
}) async {
|
||||
await _ensureInitialized();
|
||||
final uses = await _analyticsClientGetter.database.getUses(
|
||||
language,
|
||||
count: count,
|
||||
roomId: roomId,
|
||||
since: since,
|
||||
|
|
@ -257,7 +283,7 @@ class AnalyticsDataService {
|
|||
if (use.category == 'other') continue;
|
||||
|
||||
if (!cappedLastUseCache.containsKey(use.identifier)) {
|
||||
final constructs = await getConstructUse(use.identifier);
|
||||
final constructs = await getConstructUse(use.identifier, language);
|
||||
cappedLastUseCache[use.identifier] = constructs.cappedLastUse;
|
||||
}
|
||||
final cappedLastUse = cappedLastUseCache[use.identifier];
|
||||
|
|
@ -271,17 +297,20 @@ class AnalyticsDataService {
|
|||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getLocalUses() async {
|
||||
Future<List<OneConstructUse>> getLocalUses(String language) async {
|
||||
await _ensureInitialized();
|
||||
return _analyticsClientGetter.database.getLocalUses();
|
||||
return _analyticsClientGetter.database.getLocalUses(language);
|
||||
}
|
||||
|
||||
Future<int> getLocalConstructCount() async {
|
||||
Future<int> getLocalConstructCount(String language) async {
|
||||
await _ensureInitialized();
|
||||
return _analyticsClientGetter.database.getLocalConstructCount();
|
||||
return _analyticsClientGetter.database.getLocalConstructCount(language);
|
||||
}
|
||||
|
||||
Future<ConstructUses> getConstructUse(ConstructIdentifier id) async {
|
||||
Future<ConstructUses> getConstructUse(
|
||||
ConstructIdentifier id,
|
||||
String language,
|
||||
) async {
|
||||
await _ensureInitialized();
|
||||
final blocked = blockedConstructs;
|
||||
final ids = _mergeTable.groupedIds(_mergeTable.resolve(id), blocked);
|
||||
|
|
@ -294,11 +323,12 @@ class AnalyticsDataService {
|
|||
);
|
||||
}
|
||||
|
||||
return _analyticsClientGetter.database.getConstructUse(ids);
|
||||
return _analyticsClientGetter.database.getConstructUse(ids, language);
|
||||
}
|
||||
|
||||
Future<Map<ConstructIdentifier, ConstructUses>> getConstructUses(
|
||||
List<ConstructIdentifier> ids,
|
||||
String language,
|
||||
) async {
|
||||
await _ensureInitialized();
|
||||
final Map<ConstructIdentifier, List<ConstructIdentifier>> request = {};
|
||||
|
|
@ -308,14 +338,15 @@ class AnalyticsDataService {
|
|||
request[id] = _mergeTable.groupedIds(_mergeTable.resolve(id), blocked);
|
||||
}
|
||||
|
||||
return _analyticsClientGetter.database.getConstructUses(request);
|
||||
return _analyticsClientGetter.database.getConstructUses(request, language);
|
||||
}
|
||||
|
||||
Future<Map<ConstructIdentifier, ConstructUses>> getAggregatedConstructs(
|
||||
ConstructTypeEnum type,
|
||||
String language,
|
||||
) async {
|
||||
final combined = await _analyticsClientGetter.database
|
||||
.getAggregatedConstructs(type);
|
||||
.getAggregatedConstructs(type, language);
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
|
|
@ -345,6 +376,7 @@ class AnalyticsDataService {
|
|||
Future<int> getNewConstructCount(
|
||||
List<OneConstructUse> newConstructs,
|
||||
ConstructTypeEnum type,
|
||||
String language,
|
||||
) async {
|
||||
await _ensureInitialized();
|
||||
final blocked = blockedConstructs;
|
||||
|
|
@ -364,7 +396,10 @@ class AnalyticsDataService {
|
|||
constructPoints[use.identifier]! + use.xp;
|
||||
}
|
||||
|
||||
final constructs = await getConstructUses(constructPoints.keys.toList());
|
||||
final constructs = await getConstructUses(
|
||||
constructPoints.keys.toList(),
|
||||
language,
|
||||
);
|
||||
|
||||
int newConstructCount = 0;
|
||||
for (final entry in constructPoints.entries) {
|
||||
|
|
@ -377,13 +412,14 @@ class AnalyticsDataService {
|
|||
return newConstructCount;
|
||||
}
|
||||
|
||||
Future<void> updateXPOffset(int offset) async {
|
||||
Future<void> updateXPOffset(int offset, String language) async {
|
||||
_invalidateCaches();
|
||||
await _analyticsClientGetter.database.updateXPOffset(offset);
|
||||
await _analyticsClientGetter.database.updateXPOffset(offset, language);
|
||||
}
|
||||
|
||||
Future<List<AnalyticsUpdateEvent>> updateLocalAnalytics(
|
||||
AnalyticsUpdate update,
|
||||
String language,
|
||||
) async {
|
||||
final events = <AnalyticsUpdateEvent>[];
|
||||
final addedConstructs = update.addedConstructs
|
||||
|
|
@ -391,8 +427,8 @@ class AnalyticsDataService {
|
|||
.toList();
|
||||
final updateIds = addedConstructs.map((c) => c.identifier).toList();
|
||||
|
||||
final prevData = await derivedData;
|
||||
final prevConstructs = await getConstructUses(updateIds);
|
||||
final prevData = await derivedData(language);
|
||||
final prevConstructs = await getConstructUses(updateIds, language);
|
||||
|
||||
_invalidateCaches();
|
||||
await _ensureInitialized();
|
||||
|
|
@ -403,9 +439,12 @@ class AnalyticsDataService {
|
|||
.toSet();
|
||||
|
||||
_mergeTable.addConstructsByUses(addedConstructs, blocked);
|
||||
await _analyticsClientGetter.database.updateLocalAnalytics(addedConstructs);
|
||||
await _analyticsClientGetter.database.updateLocalAnalytics(
|
||||
addedConstructs,
|
||||
language,
|
||||
);
|
||||
|
||||
final newConstructs = await getConstructUses(updateIds);
|
||||
final newConstructs = await getConstructUses(updateIds, language);
|
||||
|
||||
int points = 0;
|
||||
if (updateIds.isNotEmpty) {
|
||||
|
|
@ -418,7 +457,7 @@ class AnalyticsDataService {
|
|||
}
|
||||
|
||||
final newData = prevData.addXP(points);
|
||||
await _analyticsClientGetter.database.updateDerivedStats(newData);
|
||||
await _analyticsClientGetter.database.updateDerivedStats(newData, language);
|
||||
|
||||
// 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.
|
||||
|
|
@ -444,6 +483,7 @@ class AnalyticsDataService {
|
|||
.publicProfile!
|
||||
.analytics
|
||||
.xpOffset!,
|
||||
language,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -478,30 +518,43 @@ class AnalyticsDataService {
|
|||
|
||||
Future<void> updateServerAnalytics(
|
||||
List<ConstructAnalyticsEvent> events,
|
||||
String language,
|
||||
) async {
|
||||
_invalidateCaches();
|
||||
final blocked = blockedConstructs;
|
||||
for (final event in events) {
|
||||
_mergeTable.addConstructsByUses(event.content.uses, blocked);
|
||||
}
|
||||
await _analyticsClientGetter.database.updateServerAnalytics(events);
|
||||
final vocab = await getAggregatedConstructs(ConstructTypeEnum.vocab);
|
||||
final morphs = await getAggregatedConstructs(ConstructTypeEnum.morph);
|
||||
await _analyticsClientGetter.database.updateServerAnalytics(
|
||||
events,
|
||||
language,
|
||||
);
|
||||
final vocab = await getAggregatedConstructs(
|
||||
ConstructTypeEnum.vocab,
|
||||
language,
|
||||
);
|
||||
final morphs = await getAggregatedConstructs(
|
||||
ConstructTypeEnum.morph,
|
||||
language,
|
||||
);
|
||||
final constructs = [...vocab.values, ...morphs.values];
|
||||
final totalXP = constructs.fold(0, (total, c) => total + c.points);
|
||||
|
||||
await _analyticsClientGetter.database.updateTotalXP(totalXP);
|
||||
await _analyticsClientGetter.database.updateTotalXP(totalXP, language);
|
||||
}
|
||||
|
||||
Future<void> updateBlockedConstructs(ConstructIdentifier constructId) async {
|
||||
Future<void> updateBlockedConstructs(
|
||||
ConstructIdentifier constructId,
|
||||
String language,
|
||||
) async {
|
||||
await _ensureInitialized();
|
||||
_mergeTable.removeConstruct(constructId);
|
||||
|
||||
final construct = await _analyticsClientGetter.database.getConstructUse([
|
||||
constructId,
|
||||
]);
|
||||
], language);
|
||||
|
||||
final derived = await derivedData;
|
||||
final derived = await derivedData(language);
|
||||
final newXP = derived.totalXP - construct.points;
|
||||
final newLevel = DerivedAnalyticsDataModel.calculateLevelWithXp(newXP);
|
||||
|
||||
|
|
@ -509,13 +562,13 @@ class AnalyticsDataService {
|
|||
level: newLevel,
|
||||
);
|
||||
|
||||
await _analyticsClientGetter.database.updateTotalXP(newXP);
|
||||
await _analyticsClientGetter.database.updateTotalXP(newXP, language);
|
||||
_invalidateCaches();
|
||||
}
|
||||
|
||||
Future<void> clearLocalAnalytics() async {
|
||||
Future<void> clearLocalAnalytics(String language) async {
|
||||
_invalidateCaches();
|
||||
await _ensureInitialized();
|
||||
await _analyticsClientGetter.database.clearLocalConstructData();
|
||||
await _analyticsClientGetter.database.clearLocalConstructData(language);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sqflite_common/sqflite.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
|
@ -170,30 +171,42 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
(ConstructTypeEnum.morph, false) => _aggregatedServerMorphConstructsBox,
|
||||
};
|
||||
|
||||
String _langKey(String key, String language) => '$language|$key';
|
||||
|
||||
bool _isLanguageKey(String key, String language) =>
|
||||
key.startsWith('$language|');
|
||||
|
||||
Future<String?> getUserID() => _lastEventTimestampBox.get('user_id');
|
||||
|
||||
Future<DateTime?> getLastUpdated() async {
|
||||
final entry = await _lastEventTimestampBox.get('last_updated');
|
||||
Future<DateTime?> getLastUpdated(String language) async {
|
||||
final entry = await _lastEventTimestampBox.get(
|
||||
_langKey('last_updated', language),
|
||||
);
|
||||
if (entry == null) return null;
|
||||
return DateTime.tryParse(entry);
|
||||
}
|
||||
|
||||
Future<DateTime?> getLastEventTimestamp() async {
|
||||
Future<DateTime?> getLastEventTimestamp(String language) async {
|
||||
final timestampString = await _lastEventTimestampBox.get(
|
||||
'last_event_timestamp',
|
||||
_langKey('last_event_timestamp', language),
|
||||
);
|
||||
if (timestampString == null) return null;
|
||||
return DateTime.parse(timestampString);
|
||||
}
|
||||
|
||||
Future<DerivedAnalyticsDataModel> getDerivedStats() async {
|
||||
final raw = await _derivedStatsBox.get('derived_stats');
|
||||
Future<String?> getCurrentLanguage() async {
|
||||
return _lastEventTimestampBox.get('current_language');
|
||||
}
|
||||
|
||||
Future<DerivedAnalyticsDataModel> getDerivedStats(String language) async {
|
||||
final raw = await _derivedStatsBox.get(_langKey('derived_stats', language));
|
||||
return raw == null
|
||||
? DerivedAnalyticsDataModel()
|
||||
: DerivedAnalyticsDataModel.fromJson(Map<String, dynamic>.from(raw));
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getUses({
|
||||
Future<List<OneConstructUse>> getUses(
|
||||
String language, {
|
||||
int? count,
|
||||
String? roomId,
|
||||
DateTime? since,
|
||||
|
|
@ -218,7 +231,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
}
|
||||
|
||||
// ---- Local uses ----
|
||||
final localUses = await getLocalUses()
|
||||
final localUses = await getLocalUses(language)
|
||||
..sort((a, b) => b.timeStamp.compareTo(a.timeStamp));
|
||||
|
||||
for (final use in localUses) {
|
||||
|
|
@ -232,11 +245,18 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
}
|
||||
|
||||
// ---- Server uses ----
|
||||
final serverKeys = await _serverConstructsBox.getAllKeys()
|
||||
..sort(
|
||||
(a, b) =>
|
||||
int.parse(b.split('|')[1]).compareTo(int.parse(a.split('|')[1])),
|
||||
);
|
||||
final serverKeys = (await _serverConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
// Filter out malformed or legacy keys that don't have a timestamp
|
||||
.where((key) {
|
||||
final parts = key.split('|');
|
||||
return parts.length >= 3 && int.tryParse(parts[2]) != null;
|
||||
})
|
||||
.sorted((a, b) {
|
||||
final aTimestamp = int.parse(a.split('|')[2]);
|
||||
final bTimestamp = int.parse(b.split('|')[2]);
|
||||
return bTimestamp.compareTo(aTimestamp);
|
||||
});
|
||||
|
||||
for (final key in serverKeys) {
|
||||
final serverUses = await getServerUses(key)
|
||||
|
|
@ -254,9 +274,12 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
return results;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> getLocalUses() async {
|
||||
Future<List<OneConstructUse>> getLocalUses(String language) async {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final localKeys = await _localConstructsBox.getAllKeys();
|
||||
final localKeys = (await _localConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
.toList();
|
||||
|
||||
final localValues = await _localConstructsBox.getAll(localKeys);
|
||||
for (final rawList in localValues) {
|
||||
if (rawList == null) continue;
|
||||
|
|
@ -279,24 +302,17 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
return uses;
|
||||
}
|
||||
|
||||
Future<int> getLocalConstructCount() async {
|
||||
final keys = await _localConstructsBox.getAllKeys();
|
||||
Future<int> getLocalConstructCount(String language) async {
|
||||
final keys = (await _localConstructsBox.getAllKeys()).where(
|
||||
(key) => _isLanguageKey(key, language),
|
||||
);
|
||||
return keys.length;
|
||||
}
|
||||
|
||||
Future<List<String>> getVocabConstructKeys() async {
|
||||
final serverKeys = await _aggregatedServerVocabConstructsBox.getAllKeys();
|
||||
final localKeys = await _aggregatedLocalVocabConstructsBox.getAllKeys();
|
||||
return [...serverKeys, ...localKeys];
|
||||
}
|
||||
|
||||
Future<List<String>> getMorphConstructKeys() async {
|
||||
final serverKeys = await _aggregatedServerMorphConstructsBox.getAllKeys();
|
||||
final localKeys = await _aggregatedLocalMorphConstructsBox.getAllKeys();
|
||||
return [...serverKeys, ...localKeys];
|
||||
}
|
||||
|
||||
Future<ConstructUses> getConstructUse(List<ConstructIdentifier> ids) async {
|
||||
Future<ConstructUses> getConstructUse(
|
||||
List<ConstructIdentifier> ids,
|
||||
String language,
|
||||
) async {
|
||||
assert(ids.isNotEmpty);
|
||||
|
||||
final ConstructUses construct = ConstructUses(
|
||||
|
|
@ -315,12 +331,12 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
final serverBox = _aggBox(id.type, false);
|
||||
final localBox = _aggBox(id.type, true);
|
||||
|
||||
final serverRaw = await serverBox.get(key);
|
||||
final serverRaw = await serverBox.get(_langKey(key, language));
|
||||
if (serverRaw != null) {
|
||||
server = ConstructUses.fromJson(Map<String, dynamic>.from(serverRaw));
|
||||
}
|
||||
|
||||
final localRaw = await localBox.get(key);
|
||||
final localRaw = await localBox.get(_langKey(key, language));
|
||||
if (localRaw != null) {
|
||||
local = ConstructUses.fromJson(Map<String, dynamic>.from(localRaw));
|
||||
}
|
||||
|
|
@ -333,28 +349,46 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
Future<Map<ConstructIdentifier, ConstructUses>> getConstructUses(
|
||||
Map<ConstructIdentifier, List<ConstructIdentifier>> ids,
|
||||
String language,
|
||||
) async {
|
||||
final Map<ConstructIdentifier, ConstructUses> results = {};
|
||||
for (final entry in ids.entries) {
|
||||
final construct = await getConstructUse(entry.value);
|
||||
final construct = await getConstructUse(entry.value, language);
|
||||
results[entry.key] = construct;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<void> clearLocalConstructData() async {
|
||||
Future<void> clearLocalConstructData(String language) async {
|
||||
await _transaction(() async {
|
||||
await _localConstructsBox.clear();
|
||||
await _aggregatedLocalVocabConstructsBox.clear();
|
||||
await _aggregatedLocalMorphConstructsBox.clear();
|
||||
final localKeys = (await _localConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
.toList();
|
||||
|
||||
final localVocabAggKeys =
|
||||
(await _aggregatedLocalVocabConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
.toList();
|
||||
|
||||
final localMorphAggKeys =
|
||||
(await _aggregatedLocalMorphConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
.toList();
|
||||
|
||||
await _localConstructsBox.deleteAll(localKeys);
|
||||
await _aggregatedLocalVocabConstructsBox.deleteAll(localVocabAggKeys);
|
||||
await _aggregatedLocalMorphConstructsBox.deleteAll(localMorphAggKeys);
|
||||
});
|
||||
}
|
||||
|
||||
/// Group uses by aggregate key
|
||||
Map<String, List<OneConstructUse>> _groupUses(List<OneConstructUse> uses) {
|
||||
Map<String, List<OneConstructUse>> _groupUses(
|
||||
List<OneConstructUse> uses,
|
||||
String language,
|
||||
) {
|
||||
final Map<String, List<OneConstructUse>> grouped = {};
|
||||
for (final u in uses) {
|
||||
final key = u.identifier.storageKey;
|
||||
final key = _langKey(u.identifier.storageKey, language);
|
||||
(grouped[key] ??= []).add(u);
|
||||
}
|
||||
return grouped;
|
||||
|
|
@ -405,12 +439,19 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
Future<List<ConstructUses>> getAggregatedConstructs(
|
||||
ConstructTypeEnum type,
|
||||
String language,
|
||||
) async {
|
||||
Map<String, ConstructUses> combined = {};
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final localKeys = await _aggBox(type, true).getAllKeys();
|
||||
final serverKeys = await _aggBox(type, false).getAllKeys();
|
||||
final localKeys = (await _aggBox(
|
||||
type,
|
||||
true,
|
||||
).getAllKeys()).where((key) => _isLanguageKey(key, language)).toList();
|
||||
final serverKeys = (await _aggBox(
|
||||
type,
|
||||
false,
|
||||
).getAllKeys()).where((key) => _isLanguageKey(key, language)).toList();
|
||||
|
||||
final serverValues = await _aggBox(type, false).getAll(serverKeys);
|
||||
final serverConstructs = serverValues
|
||||
|
|
@ -456,45 +497,65 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> updateLastUpdated(DateTime timestamp) {
|
||||
Future<void> updateCurrentLanguage(String language) {
|
||||
return _transaction(() async {
|
||||
await _lastEventTimestampBox.put('current_language', language);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateLastUpdated(DateTime timestamp, String language) async {
|
||||
return _transaction(() async {
|
||||
await _lastEventTimestampBox.put(
|
||||
'last_updated',
|
||||
_langKey('last_updated', language),
|
||||
timestamp.toIso8601String(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateXPOffset(int offset) {
|
||||
Future<void> updateXPOffset(int offset, String language) async {
|
||||
return _transaction(() async {
|
||||
final stats = await getDerivedStats();
|
||||
final stats = await getDerivedStats(language);
|
||||
final updatedStats = stats.copyWithOffset(offset);
|
||||
await _derivedStatsBox.put('derived_stats', updatedStats.toJson());
|
||||
await _derivedStatsBox.put(
|
||||
_langKey('derived_stats', language),
|
||||
updatedStats.toJson(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTotalXP(int totalXP) {
|
||||
Future<void> updateTotalXP(int totalXP, String language) {
|
||||
return _transaction(() async {
|
||||
final stats = await getDerivedStats();
|
||||
final stats = await getDerivedStats(language);
|
||||
final updatedStats = stats.copyWithTotalXP(totalXP);
|
||||
await _derivedStatsBox.put('derived_stats', updatedStats.toJson());
|
||||
await _derivedStatsBox.put(
|
||||
_langKey('derived_stats', language),
|
||||
updatedStats.toJson(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateDerivedStats(DerivedAnalyticsDataModel newStats) =>
|
||||
_derivedStatsBox.put('derived_stats', newStats.toJson());
|
||||
Future<void> updateDerivedStats(
|
||||
DerivedAnalyticsDataModel newStats,
|
||||
String language,
|
||||
) => _derivedStatsBox.put(
|
||||
_langKey('derived_stats', language),
|
||||
newStats.toJson(),
|
||||
);
|
||||
|
||||
Future<void> updateServerAnalytics(
|
||||
List<ConstructAnalyticsEvent> events,
|
||||
String language,
|
||||
) async {
|
||||
if (events.isEmpty) return;
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
await _transaction(() async {
|
||||
final lastUpdated = await getLastEventTimestamp();
|
||||
final lastUpdated = await getLastEventTimestamp(language);
|
||||
|
||||
DateTime mostRecent = lastUpdated ?? events.first.event.originServerTs;
|
||||
final existingKeys = (await _serverConstructsBox.getAllKeys()).toSet();
|
||||
final existingKeys = (await _serverConstructsBox.getAllKeys())
|
||||
.where((key) => _isLanguageKey(key, language))
|
||||
.toSet();
|
||||
|
||||
final List<OneConstructUse> aggregatedVocabUses = [];
|
||||
final List<OneConstructUse> aggregatedMorphUses = [];
|
||||
|
|
@ -508,7 +569,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
).toString();
|
||||
|
||||
if (lastUpdated != null && ts.isBefore(lastUpdated)) continue;
|
||||
if (existingKeys.contains(key)) continue;
|
||||
if (existingKeys.contains(_langKey(key, language))) continue;
|
||||
|
||||
if (ts.isAfter(mostRecent)) mostRecent = ts;
|
||||
|
||||
|
|
@ -525,7 +586,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
// Write events sequentially
|
||||
for (final e in pendingWrites.entries) {
|
||||
_serverConstructsBox.put(
|
||||
e.key,
|
||||
_langKey(e.key, language),
|
||||
e.value.map((u) => u.toJson()).toList(),
|
||||
);
|
||||
}
|
||||
|
|
@ -533,7 +594,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
// Update aggregates
|
||||
final aggVocabUpdates = await _aggregateFromBox(
|
||||
_aggregatedServerVocabConstructsBox,
|
||||
_groupUses(aggregatedVocabUses),
|
||||
_groupUses(aggregatedVocabUses, language),
|
||||
);
|
||||
|
||||
for (final entry in aggVocabUpdates.entries) {
|
||||
|
|
@ -545,7 +606,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
final aggMorphUpdates = await _aggregateFromBox(
|
||||
_aggregatedServerMorphConstructsBox,
|
||||
_groupUses(aggregatedMorphUses),
|
||||
_groupUses(aggregatedMorphUses, language),
|
||||
);
|
||||
|
||||
for (final entry in aggMorphUpdates.entries) {
|
||||
|
|
@ -557,12 +618,12 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
// Update timestamp
|
||||
await _lastEventTimestampBox.put(
|
||||
'last_event_timestamp',
|
||||
_langKey('last_event_timestamp', language),
|
||||
mostRecent.toIso8601String(),
|
||||
);
|
||||
});
|
||||
|
||||
await updateLastUpdated(DateTime.now());
|
||||
await _updateLastUpdated(DateTime.now(), language);
|
||||
|
||||
stopwatch.stop();
|
||||
Logs().i(
|
||||
|
|
@ -570,7 +631,10 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> updateLocalAnalytics(List<OneConstructUse> uses) async {
|
||||
Future<void> updateLocalAnalytics(
|
||||
List<OneConstructUse> uses,
|
||||
String language,
|
||||
) async {
|
||||
if (uses.isEmpty) return;
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
|
@ -578,7 +642,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
// Store local constructs
|
||||
final key = DateTime.now().millisecondsSinceEpoch;
|
||||
_localConstructsBox.put(
|
||||
key.toString(),
|
||||
_langKey(key.toString(), language),
|
||||
uses.map((u) => u.toJson()).toList(),
|
||||
);
|
||||
|
||||
|
|
@ -593,7 +657,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
// Update aggregates
|
||||
final aggVocabUpdates = await _aggregateFromBox(
|
||||
_aggregatedLocalVocabConstructsBox,
|
||||
_groupUses(vocabUses),
|
||||
_groupUses(vocabUses, language),
|
||||
);
|
||||
|
||||
for (final entry in aggVocabUpdates.entries) {
|
||||
|
|
@ -605,7 +669,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
final aggMorphUpdates = await _aggregateFromBox(
|
||||
_aggregatedLocalMorphConstructsBox,
|
||||
_groupUses(morphUses),
|
||||
_groupUses(morphUses, language),
|
||||
);
|
||||
|
||||
for (final entry in aggMorphUpdates.entries) {
|
||||
|
|
@ -616,7 +680,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
}
|
||||
});
|
||||
|
||||
await updateLastUpdated(DateTime.now());
|
||||
await _updateLastUpdated(DateTime.now(), language);
|
||||
|
||||
stopwatch.stop();
|
||||
Logs().i("Local analytics update took ${stopwatch.elapsedMilliseconds} ms");
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
|||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -41,6 +42,8 @@ class AnalyticsSyncController {
|
|||
|
||||
AnalyticsSyncController({required this.client, required this.dataService});
|
||||
|
||||
LanguageModel? get _l2 => MatrixState.pangeaController.userController.userL2;
|
||||
|
||||
void start() {
|
||||
_subscription ??= client.onSync.stream.listen(_onSync);
|
||||
}
|
||||
|
|
@ -52,13 +55,19 @@ class AnalyticsSyncController {
|
|||
|
||||
Future<void> _onSync(SyncUpdate update) async {
|
||||
final analyticsRoom = _getAnalyticsRoom();
|
||||
if (analyticsRoom == null) return;
|
||||
final l2 = _l2;
|
||||
if (analyticsRoom == null || l2 == null) return;
|
||||
|
||||
final roomUpdates = update.rooms?.join?[analyticsRoom.id]?.timeline?.events;
|
||||
if (roomUpdates == null) return;
|
||||
|
||||
for (final type in _AnalyticsUpdateEvent.values) {
|
||||
await _dispatchSyncEvents(type, roomUpdates, analyticsRoom);
|
||||
await _dispatchSyncEvents(
|
||||
type,
|
||||
roomUpdates,
|
||||
analyticsRoom,
|
||||
l2.langCodeShort,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +75,7 @@ class AnalyticsSyncController {
|
|||
_AnalyticsUpdateEvent type,
|
||||
List<MatrixEvent> events,
|
||||
Room analyticsRoom,
|
||||
String language,
|
||||
) async {
|
||||
final updates = events
|
||||
.where((e) => e.type == type.eventType && e.senderId == client.userID)
|
||||
|
|
@ -73,7 +83,7 @@ class AnalyticsSyncController {
|
|||
|
||||
switch (type) {
|
||||
case _AnalyticsUpdateEvent.constructAnalytics:
|
||||
await _onConstructEvents(updates, analyticsRoom);
|
||||
await _onConstructEvents(updates, analyticsRoom, language);
|
||||
break;
|
||||
case _AnalyticsUpdateEvent.activityAnalytics:
|
||||
_onActivityEvents(updates);
|
||||
|
|
@ -82,7 +92,7 @@ class AnalyticsSyncController {
|
|||
_onLemmaInfoEvents(updates);
|
||||
break;
|
||||
case _AnalyticsUpdateEvent.blockedConstruct:
|
||||
await _onBlockedConstructEvents(updates);
|
||||
await _onBlockedConstructEvents(updates, language);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +100,7 @@ class AnalyticsSyncController {
|
|||
Future<void> _onConstructEvents(
|
||||
List<MatrixEvent> events,
|
||||
Room analyticsRoom,
|
||||
String language,
|
||||
) async {
|
||||
final constructEvents = events
|
||||
.map(
|
||||
|
|
@ -103,6 +114,7 @@ class AnalyticsSyncController {
|
|||
if (constructEvents.isEmpty) return;
|
||||
await dataService.updateDispatcher.sendServerAnalyticsUpdate(
|
||||
constructEvents,
|
||||
language,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +151,10 @@ class AnalyticsSyncController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _onBlockedConstructEvents(List<MatrixEvent> events) async {
|
||||
Future<void> _onBlockedConstructEvents(
|
||||
List<MatrixEvent> events,
|
||||
String language,
|
||||
) async {
|
||||
for (final event in events) {
|
||||
final current = AnalyticsSettingsModel.fromJson(event.content);
|
||||
final prevContent =
|
||||
|
|
@ -155,6 +170,7 @@ class AnalyticsSyncController {
|
|||
if (newlyBlocked.isEmpty) continue;
|
||||
await dataService.updateDispatcher.sendBlockedConstructsUpdate(
|
||||
newlyBlocked.toSet(),
|
||||
language,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -176,11 +192,11 @@ class AnalyticsSyncController {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> bulkUpdate() async {
|
||||
Future<void> bulkUpdate(String language) async {
|
||||
final analyticsRoom = _getAnalyticsRoom();
|
||||
if (analyticsRoom == null) return;
|
||||
|
||||
final lastUpdated = await dataService.getLastUpdatedAnalytics();
|
||||
final lastUpdated = await dataService.getLastUpdatedAnalytics(language);
|
||||
|
||||
final events = await analyticsRoom.getAnalyticsEvents(
|
||||
userId: client.userID!,
|
||||
|
|
@ -189,11 +205,11 @@ class AnalyticsSyncController {
|
|||
|
||||
if (events == null || events.isEmpty) return;
|
||||
|
||||
await dataService.updateServerAnalytics(events);
|
||||
await dataService.updateServerAnalytics(events, language);
|
||||
}
|
||||
|
||||
Room? _getAnalyticsRoom() {
|
||||
final l2 = MatrixState.pangeaController.userController.userL2;
|
||||
final l2 = _l2;
|
||||
if (l2 == null) return null;
|
||||
return client.analyticsRoomLocal(l2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,10 @@ class AnalyticsUpdateDispatcher {
|
|||
|
||||
Future<void> sendBlockedConstructsUpdate(
|
||||
Set<ConstructIdentifier> blockedConstructs,
|
||||
String language,
|
||||
) async {
|
||||
for (final blockedConstruct in blockedConstructs) {
|
||||
await dataService.updateBlockedConstructs(blockedConstruct);
|
||||
await dataService.updateBlockedConstructs(blockedConstruct, language);
|
||||
}
|
||||
final update = AnalyticsStreamUpdate(blockedConstructs: blockedConstructs);
|
||||
constructUpdateStream.add(update);
|
||||
|
|
@ -101,13 +102,20 @@ class AnalyticsUpdateDispatcher {
|
|||
|
||||
Future<void> sendServerAnalyticsUpdate(
|
||||
List<ConstructAnalyticsEvent> events,
|
||||
String language,
|
||||
) async {
|
||||
await dataService.updateServerAnalytics(events);
|
||||
await dataService.updateServerAnalytics(events, language);
|
||||
sendEmptyAnalyticsUpdate();
|
||||
}
|
||||
|
||||
Future<void> sendLocalAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
|
||||
final events = await dataService.updateLocalAnalytics(analyticsUpdate);
|
||||
Future<void> sendLocalAnalyticsUpdate(
|
||||
AnalyticsUpdate analyticsUpdate,
|
||||
String language,
|
||||
) async {
|
||||
final events = await dataService.updateLocalAnalytics(
|
||||
analyticsUpdate,
|
||||
language,
|
||||
);
|
||||
for (final event in events) {
|
||||
_dispatch(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class AnalyticsUpdateService {
|
|||
await sendLocalAnalyticsToAnalyticsRoom(l2Override: update.prevTargetLang);
|
||||
await dataService.reinitialize();
|
||||
|
||||
final data = await dataService.derivedData;
|
||||
final data = await dataService.derivedData(update.targetLang.langCodeShort);
|
||||
MatrixState.pangeaController.userController.updateAnalyticsProfile(
|
||||
level: data.level,
|
||||
);
|
||||
|
|
@ -59,15 +59,19 @@ class AnalyticsUpdateService {
|
|||
|
||||
Future<void> addAnalytics(
|
||||
String? targetID,
|
||||
List<OneConstructUse> newConstructs, {
|
||||
List<OneConstructUse> newConstructs,
|
||||
String language, {
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
await dataService.updateDispatcher.sendLocalAnalyticsUpdate(
|
||||
AnalyticsUpdate(newConstructs, targetID: targetID),
|
||||
language,
|
||||
);
|
||||
|
||||
final localConstructCount = await dataService.getLocalConstructCount();
|
||||
final lastUpdated = await dataService.getLastUpdatedAnalytics();
|
||||
final localConstructCount = await dataService.getLocalConstructCount(
|
||||
language,
|
||||
);
|
||||
final lastUpdated = await dataService.getLastUpdatedAnalytics(language);
|
||||
final difference = DateTime.now().difference(lastUpdated ?? DateTime.now());
|
||||
|
||||
if (forceUpdate ||
|
||||
|
|
@ -80,6 +84,16 @@ class AnalyticsUpdateService {
|
|||
Future<void> sendLocalAnalyticsToAnalyticsRoom({
|
||||
LanguageModel? l2Override,
|
||||
}) async {
|
||||
final lang = l2Override ?? _l2;
|
||||
if (lang == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "No L2 language set for user",
|
||||
m: "Cannot send local analytics to analytics room",
|
||||
data: {"l2Override": l2Override},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final inProgress =
|
||||
_updateCompleter != null && !_updateCompleter!.isCompleted;
|
||||
|
||||
|
|
@ -90,8 +104,8 @@ class AnalyticsUpdateService {
|
|||
|
||||
_updateCompleter = Completer<void>();
|
||||
try {
|
||||
await _updateAnalytics(l2Override: l2Override);
|
||||
await dataService.clearLocalAnalytics();
|
||||
await _updateAnalytics(lang);
|
||||
await dataService.clearLocalAnalytics(lang.langCodeShort);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
@ -105,13 +119,15 @@ class AnalyticsUpdateService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _updateAnalytics({LanguageModel? l2Override}) async {
|
||||
final localConstructs = await dataService.getLocalUses();
|
||||
Future<void> _updateAnalytics(LanguageModel language) async {
|
||||
final localConstructs = await dataService.getLocalUses(
|
||||
language.langCodeShort,
|
||||
);
|
||||
if (localConstructs.isEmpty) return;
|
||||
final analyticsRoom = await _getAnalyticsRoom(l2Override: l2Override);
|
||||
final analyticsRoom = await _getAnalyticsRoom(l2Override: language);
|
||||
if (analyticsRoom == null) {
|
||||
debugPrint(
|
||||
"No analytics room found for L2 Override: ${l2Override?.langCode}",
|
||||
"No analytics room found for L2 Override: ${language.langCodeShort}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,12 @@ mixin AnalyticsUpdater<T extends StatefulWidget> on State<T> {
|
|||
Future<void> addAnalytics(
|
||||
List<OneConstructUse> constructs,
|
||||
String? targetId,
|
||||
) => Matrix.of(
|
||||
context,
|
||||
).analyticsDataService.updateService.addAnalytics(targetId, constructs);
|
||||
String language,
|
||||
) => Matrix.of(context).analyticsDataService.updateService.addAnalytics(
|
||||
targetId,
|
||||
constructs,
|
||||
language,
|
||||
);
|
||||
|
||||
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
|
||||
if (update.targetID != null) {
|
||||
|
|
|
|||
|
|
@ -28,10 +28,18 @@ class LevelUpAnalyticsService {
|
|||
) async {
|
||||
await ensureInitialized();
|
||||
|
||||
final uses = await dataService.getUses(since: lastLevelUpTimestamp);
|
||||
final userController = MatrixState.pangeaController.userController;
|
||||
final l2 = userController.userL2;
|
||||
if (l2 == null) {
|
||||
throw Exception("No L2 language set for user");
|
||||
}
|
||||
|
||||
final uses = await dataService.getUses(
|
||||
l2.langCodeShort,
|
||||
since: lastLevelUpTimestamp,
|
||||
);
|
||||
final messages = await _buildMessageContext(uses);
|
||||
|
||||
final userController = MatrixState.pangeaController.userController;
|
||||
final request = ConstructSummaryRequest(
|
||||
constructs: uses,
|
||||
messages: messages,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_models.dart';
|
||||
|
|
@ -82,8 +83,19 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
LanguageModel? get _l2 => MatrixState.pangeaController.userController.userL2;
|
||||
|
||||
Future<void> _setAnalyticsData() async {
|
||||
final future = <Future>[_setMorphs(), _setVocab()];
|
||||
final l2 = _l2;
|
||||
if (l2 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "No L2 language set for user",
|
||||
m: "Cannot set analytics data",
|
||||
data: {"view": widget.view, "construct": widget.construct},
|
||||
);
|
||||
return;
|
||||
}
|
||||
final future = <Future>[_setMorphs(), _setVocab(l2.langCodeShort)];
|
||||
await Future.wait(future);
|
||||
}
|
||||
|
||||
|
|
@ -104,11 +116,12 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _setVocab() async {
|
||||
Future<void> _setVocab(String language) async {
|
||||
try {
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
final data = await analyticsService.getAggregatedConstructs(
|
||||
ConstructTypeEnum.vocab,
|
||||
language,
|
||||
);
|
||||
|
||||
vocab = data.values.toList();
|
||||
|
|
@ -128,11 +141,7 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
morphs = resp;
|
||||
features = resp.displayFeatures;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {"l2": MatrixState.pangeaController.userController.userL2},
|
||||
);
|
||||
ErrorHandler.logError(e: e, s: s, data: {"l2": _l2?.langCode});
|
||||
} finally {
|
||||
features.sort(
|
||||
(a, b) => morphFeatureSortOrder
|
||||
|
|
@ -214,9 +223,11 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
PTRequest ptRequest,
|
||||
PTResponse ptResponse,
|
||||
) async {
|
||||
final l2 = _l2;
|
||||
if (l2 == null) return;
|
||||
final requestData = TokenInfoFeedbackRequestData(
|
||||
userId: Matrix.of(context).client.userID!,
|
||||
detectedLanguage: MatrixState.pangeaController.userController.userL2Code!,
|
||||
detectedLanguage: l2.langCode,
|
||||
tokens: [token],
|
||||
selectedToken: 0,
|
||||
wordCardL1: MatrixState.pangeaController.userController.userL1Code!,
|
||||
|
|
@ -228,7 +239,7 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
await TokenFeedbackUtil.showTokenFeedbackDialog(
|
||||
context,
|
||||
requestData: requestData,
|
||||
langCode: MatrixState.pangeaController.userController.userL2Code!,
|
||||
langCode: l2.langCode,
|
||||
onUpdated: () => reloadNotifier.value++,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
|
|
@ -23,12 +24,23 @@ class ConstructXPProgressBar extends StatelessWidget {
|
|||
);
|
||||
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
|
||||
return StreamBuilder(
|
||||
stream: analyticsService.updateDispatcher.constructUpdateStream.stream,
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder(
|
||||
future: analyticsService.getConstructUse(construct),
|
||||
future: l2 != null
|
||||
? analyticsService.getConstructUse(construct, l2)
|
||||
: Future.value(
|
||||
ConstructUses(
|
||||
uses: [],
|
||||
constructType: construct.type,
|
||||
lemma: construct.lemma,
|
||||
category: construct.category,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final points = snapshot.data?.points ?? 0;
|
||||
final progress = min(1.0, points / AnalyticsConstants.xpForFlower);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const padding = EdgeInsets.symmetric(vertical: 10.0);
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -54,7 +56,7 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final feature = controller.features[index];
|
||||
return feature.displayTags.isNotEmpty
|
||||
return feature.displayTags.isNotEmpty && l2 != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: MorphFeatureBox(
|
||||
|
|
@ -62,6 +64,7 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
allTags: controller.morphs
|
||||
.getDisplayTags(feature.feature)
|
||||
.toSet(),
|
||||
language: l2,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
|
@ -79,11 +82,13 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
class MorphFeatureBox extends StatelessWidget {
|
||||
final String morphFeature;
|
||||
final Set<String> allTags;
|
||||
final String language;
|
||||
|
||||
const MorphFeatureBox({
|
||||
super.key,
|
||||
required this.morphFeature,
|
||||
required this.allTags,
|
||||
required this.language,
|
||||
});
|
||||
|
||||
MorphFeaturesEnum get feature =>
|
||||
|
|
@ -140,7 +145,7 @@ class MorphFeatureBox extends StatelessWidget {
|
|||
);
|
||||
|
||||
return FutureBuilder(
|
||||
future: analyticsService.getConstructUse(id),
|
||||
future: analyticsService.getConstructUse(id, language),
|
||||
builder: (context, snapshot) => MorphTagChip(
|
||||
morphFeature: morphFeature,
|
||||
morphTag: morphTag,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_usage_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/construct_xp_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_feature_display.dart';
|
||||
|
|
@ -22,10 +23,21 @@ class MorphDetailsView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
return FutureBuilder(
|
||||
future: Matrix.of(
|
||||
context,
|
||||
).analyticsDataService.getConstructUse(constructId),
|
||||
future: l2 != null
|
||||
? Matrix.of(
|
||||
context,
|
||||
).analyticsDataService.getConstructUse(constructId, l2)
|
||||
: Future.value(
|
||||
ConstructUses(
|
||||
uses: [],
|
||||
lemma: constructId.lemma,
|
||||
category: constructId.category,
|
||||
constructType: constructId.type,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final construct = snapshot.data;
|
||||
final level = construct?.lemmaCategory ?? ConstructLevelEnum.seeds;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popu
|
|||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_usage_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/construct_xp_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
|
@ -31,9 +32,20 @@ class VocabDetailsView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
return FutureBuilder(
|
||||
future: analyticsService.getConstructUse(constructId),
|
||||
future: l2 != null
|
||||
? analyticsService.getConstructUse(constructId, l2)
|
||||
: Future.value(
|
||||
ConstructUses(
|
||||
uses: [],
|
||||
constructType: constructId.type,
|
||||
lemma: constructId.lemma,
|
||||
category: constructId.category,
|
||||
),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final construct = snapshot.data;
|
||||
final level = construct?.lemmaCategory ?? ConstructLevelEnum.seeds;
|
||||
|
|
|
|||
|
|
@ -129,9 +129,15 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
}
|
||||
|
||||
Future<List<AnalyticsSummaryModel>> _getVocabAnalytics() async {
|
||||
final l2 = MatrixState.pangeaController.userController.userL2;
|
||||
if (l2 == null) {
|
||||
throw Exception("No L2 set for user");
|
||||
}
|
||||
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
final aggregatedVocab = await analyticsService.getAggregatedConstructs(
|
||||
ConstructTypeEnum.vocab,
|
||||
l2.langCodeShort,
|
||||
);
|
||||
|
||||
final uses = aggregatedVocab.values.toList();
|
||||
|
|
@ -182,6 +188,10 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
}
|
||||
|
||||
Future<List<AnalyticsSummaryModel>> _getMorphAnalytics() async {
|
||||
final l2 = MatrixState.pangeaController.userController.userL2;
|
||||
if (l2 == null) {
|
||||
throw Exception("No L2 set for user");
|
||||
}
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
|
||||
final morphs = await MorphsRepo.get();
|
||||
|
|
@ -199,7 +209,10 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
category: feature.feature,
|
||||
);
|
||||
|
||||
final uses = await analyticsService.getConstructUse(id);
|
||||
final uses = await analyticsService.getConstructUse(
|
||||
id,
|
||||
l2.langCodeShort,
|
||||
);
|
||||
|
||||
final xp = uses.points;
|
||||
final exampleMessages = await _getExampleMessages([uses]);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ mixin LemmaEmojiSetter {
|
|||
if (constructId.userSetEmoji == null) {
|
||||
_getEmojiAnalytics(
|
||||
constructId,
|
||||
language: langCode.split("-").first,
|
||||
targetId: targetId,
|
||||
roomId: roomId,
|
||||
eventId: eventId,
|
||||
|
|
@ -94,6 +95,7 @@ mixin LemmaEmojiSetter {
|
|||
|
||||
void _getEmojiAnalytics(
|
||||
ConstructIdentifier constructId, {
|
||||
required String language,
|
||||
String? eventId,
|
||||
String? roomId,
|
||||
String? targetId,
|
||||
|
|
@ -115,6 +117,6 @@ mixin LemmaEmojiSetter {
|
|||
];
|
||||
|
||||
MatrixState.pangeaController.matrixState.analyticsDataService.updateService
|
||||
.addAnalytics(targetId, constructs);
|
||||
.addAnalytics(targetId, constructs, language);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart
|
|||
import 'package:fluffychat/pangea/analytics_misc/level_summary_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -174,8 +175,13 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
);
|
||||
_constructSummaryCompleter.complete(summary);
|
||||
analyticsRoom.setLevelUpSummary(summary);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
debugPrint("Error generating level up analytics: $e");
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {"level": widget.level, "prevLevel": widget.prevLevel},
|
||||
);
|
||||
_constructSummaryCompleter.completeError(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dar
|
|||
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
|
|
@ -63,8 +64,12 @@ class SessionLoader extends AsyncLoader<AnalyticsPracticeSessionModel> {
|
|||
SessionLoader({required this.type});
|
||||
|
||||
@override
|
||||
Future<AnalyticsPracticeSessionModel> fetch() =>
|
||||
AnalyticsPracticeSessionRepo.get(type);
|
||||
Future<AnalyticsPracticeSessionModel> fetch() {
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
if (l2 == null) throw Exception('User L2 language not set');
|
||||
return AnalyticsPracticeSessionRepo.get(type, l2);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsPractice extends StatefulWidget {
|
||||
|
|
@ -153,6 +158,8 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
AnalyticsDataService get _analyticsService =>
|
||||
Matrix.of(context).analyticsDataService;
|
||||
|
||||
LanguageModel? get _l2 => MatrixState.pangeaController.userController.userL2;
|
||||
|
||||
List<VocabPracticeChoice> filteredChoices(
|
||||
MultipleChoicePracticeActivityModel activity,
|
||||
) {
|
||||
|
|
@ -248,7 +255,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
TtsController.tryToSpeak(
|
||||
activityTarget.value!.target.tokens.first.vocabConstructID.lemma,
|
||||
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
langCode: _l2!.langCode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -324,6 +331,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
await _analyticsService.updateService.addAnalytics(
|
||||
null,
|
||||
bonus,
|
||||
_l2!.langCodeShort,
|
||||
forceUpdate: true,
|
||||
);
|
||||
AnalyticsPractice.bypassExitConfirmation = true;
|
||||
|
|
@ -534,7 +542,9 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
xp: 0,
|
||||
);
|
||||
|
||||
await _analyticsService.updateService.addAnalytics(null, [use]);
|
||||
await _analyticsService.updateService.addAnalytics(null, [
|
||||
use,
|
||||
], _l2!.langCodeShort);
|
||||
}
|
||||
|
||||
void onHintPressed() {
|
||||
|
|
@ -611,6 +621,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
await _analyticsService.updateService.addAnalytics(
|
||||
choiceTargetId(choiceContent),
|
||||
[use],
|
||||
_l2!.langCodeShort,
|
||||
);
|
||||
|
||||
if (!isCorrect) return;
|
||||
|
|
@ -663,7 +674,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
|
||||
return ExampleMessageUtil.getExampleMessage(
|
||||
await _analyticsService.getConstructUse(construct),
|
||||
await _analyticsService.getConstructUse(construct, _l2!.langCodeShort),
|
||||
Matrix.of(context).client,
|
||||
);
|
||||
}
|
||||
|
|
@ -677,7 +688,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
|
||||
Future<DerivedAnalyticsDataModel> get derivedAnalyticsData =>
|
||||
_analyticsService.derivedData;
|
||||
_analyticsService.derivedData(_l2!.langCodeShort);
|
||||
|
||||
/// Returns congratulations message based on performance
|
||||
String getCompletionMessage(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class InsufficientDataException implements Exception {}
|
|||
class AnalyticsPracticeSessionRepo {
|
||||
static Future<AnalyticsPracticeSessionModel> get(
|
||||
ConstructTypeEnum type,
|
||||
String language,
|
||||
) async {
|
||||
if (MatrixState.pangeaController.subscriptionController.isSubscribed ==
|
||||
false) {
|
||||
|
|
@ -43,12 +44,12 @@ class AnalyticsPracticeSessionRepo {
|
|||
final halfNeeded = (totalNeeded / 2).ceil();
|
||||
|
||||
// Fetch audio constructs (with example messages)
|
||||
final audioMap = await _fetchAudio();
|
||||
final audioMap = await _fetchAudio(language);
|
||||
final audioCount = min(audioMap.length, halfNeeded);
|
||||
|
||||
// Fetch vocab constructs to fill the rest
|
||||
final vocabNeeded = totalNeeded - audioCount;
|
||||
final vocabConstructs = await _fetchVocab();
|
||||
final vocabConstructs = await _fetchVocab(language);
|
||||
final vocabCount = min(vocabConstructs.length, vocabNeeded);
|
||||
|
||||
for (final entry in audioMap.entries.take(audioCount)) {
|
||||
|
|
@ -74,12 +75,12 @@ class AnalyticsPracticeSessionRepo {
|
|||
}
|
||||
targets.shuffle();
|
||||
} else {
|
||||
final errorTargets = await _fetchErrors();
|
||||
final errorTargets = await _fetchErrors(language);
|
||||
targets.addAll(errorTargets);
|
||||
if (targets.length <
|
||||
(AnalyticsPracticeConstants.practiceGroupSize +
|
||||
AnalyticsPracticeConstants.errorBufferSize)) {
|
||||
final morphs = await _fetchMorphs();
|
||||
final morphs = await _fetchMorphs(language);
|
||||
final remainingCount =
|
||||
(AnalyticsPracticeConstants.practiceGroupSize +
|
||||
AnalyticsPracticeConstants.errorBufferSize) -
|
||||
|
|
@ -118,12 +119,12 @@ class AnalyticsPracticeSessionRepo {
|
|||
return session;
|
||||
}
|
||||
|
||||
static Future<List<ConstructIdentifier>> _fetchVocab() async {
|
||||
static Future<List<ConstructIdentifier>> _fetchVocab(String language) async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab, language)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
|
|
@ -151,13 +152,14 @@ class AnalyticsPracticeSessionRepo {
|
|||
return targets;
|
||||
}
|
||||
|
||||
static Future<Map<ConstructIdentifier, AudioExampleMessage>>
|
||||
_fetchAudio() async {
|
||||
static Future<Map<ConstructIdentifier, AudioExampleMessage>> _fetchAudio(
|
||||
String language,
|
||||
) async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab, language)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
|
|
@ -187,7 +189,7 @@ class AnalyticsPracticeSessionRepo {
|
|||
final audioExampleMessage =
|
||||
await ExampleMessageUtil.getAudioExampleMessage(
|
||||
await MatrixState.pangeaController.matrixState.analyticsDataService
|
||||
.getConstructUse(construct.id),
|
||||
.getConstructUse(construct.id, language),
|
||||
MatrixState.pangeaController.matrixState.client,
|
||||
noBold: true,
|
||||
);
|
||||
|
|
@ -209,12 +211,12 @@ class AnalyticsPracticeSessionRepo {
|
|||
return targets;
|
||||
}
|
||||
|
||||
static Future<List<MorphPracticeTarget>> _fetchMorphs() async {
|
||||
static Future<List<MorphPracticeTarget>> _fetchMorphs(String language) async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.morph)
|
||||
.getAggregatedConstructs(ConstructTypeEnum.morph, language)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
final morphInfoRequest = MorphInfoRequest(
|
||||
|
|
@ -288,7 +290,7 @@ class AnalyticsPracticeSessionRepo {
|
|||
|
||||
exampleMessage = await ExampleMessageUtil.getExampleMessage(
|
||||
await MatrixState.pangeaController.matrixState.analyticsDataService
|
||||
.getConstructUse(entry.id),
|
||||
.getConstructUse(entry.id, language),
|
||||
MatrixState.pangeaController.matrixState.client,
|
||||
form: form,
|
||||
);
|
||||
|
|
@ -318,12 +320,15 @@ class AnalyticsPracticeSessionRepo {
|
|||
return targets;
|
||||
}
|
||||
|
||||
static Future<List<AnalyticsActivityTarget>> _fetchErrors() async {
|
||||
static Future<List<AnalyticsActivityTarget>> _fetchErrors(
|
||||
String language,
|
||||
) async {
|
||||
final allRecentUses = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getUses(
|
||||
language,
|
||||
count: 300,
|
||||
filterCapped: false,
|
||||
types: [
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class VocabAudioActivityGenerator {
|
|||
final choices = await LemmaActivityGenerator.lemmaActivityDistractors(
|
||||
token,
|
||||
maxChoices: 20,
|
||||
language: req.userL2.split('-').first,
|
||||
);
|
||||
final choicesList = choices
|
||||
.map((c) => c.lemma)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class VocabMeaningActivityGenerator {
|
|||
final token = req.target.tokens.first;
|
||||
final choices = await LemmaActivityGenerator.lemmaActivityDistractors(
|
||||
token,
|
||||
language: req.userL2.split('-').first,
|
||||
);
|
||||
|
||||
if (!choices.contains(token.vocabConstructID)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -199,7 +200,13 @@ class LearningProgressIndicators extends StatelessWidget {
|
|||
}
|
||||
: null,
|
||||
child: FutureBuilder(
|
||||
future: analyticsService.derivedData,
|
||||
future: userL2 != null
|
||||
? analyticsService.derivedData(
|
||||
userL2.langCodeShort,
|
||||
)
|
||||
: Future.value(
|
||||
DerivedAnalyticsDataModel(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final cached =
|
||||
analyticsService.cachedDerivedData;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import 'package:flutter/material.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_data/derived_analytics_data_model.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_summary/learning_progress_indicators.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
|
|
@ -19,6 +21,8 @@ class LevelAnalyticsDetailsContent extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
final language =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
|
|
@ -35,7 +39,9 @@ class LevelAnalyticsDetailsContent extends StatelessWidget {
|
|||
canSelect: false,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: analyticsService.derivedData,
|
||||
future: language != null
|
||||
? analyticsService.derivedData(language)
|
||||
: Future.value(DerivedAnalyticsDataModel()),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
|
|
@ -69,8 +75,10 @@ class LevelAnalyticsDetailsContent extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: analyticsService.getUses(count: 100),
|
||||
child: FutureBuilder<List<OneConstructUse>>(
|
||||
future: language != null
|
||||
? analyticsService.getUses(language, count: 100)
|
||||
: Future.value(<OneConstructUse>[]),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ class LemmaActivityGenerator {
|
|||
debugger(when: kDebugMode && req.target.tokens.length != 1);
|
||||
|
||||
final token = req.target.tokens.first;
|
||||
final choices = await lemmaActivityDistractors(token);
|
||||
final choices = await lemmaActivityDistractors(
|
||||
token,
|
||||
language: req.userL2.split('-').first,
|
||||
);
|
||||
|
||||
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
|
||||
return MessageActivityResponse(
|
||||
|
|
@ -32,13 +35,14 @@ class LemmaActivityGenerator {
|
|||
|
||||
static Future<Set<ConstructIdentifier>> lemmaActivityDistractors(
|
||||
PangeaToken token, {
|
||||
required String language,
|
||||
int? maxChoices = 4,
|
||||
}) async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab);
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab, language);
|
||||
|
||||
final List<ConstructIdentifier> constructIds = constructs.keys.toList();
|
||||
// Offload computation to an isolate
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@ class PracticeSelectionRepo {
|
|||
if (eligibleTokens.isEmpty) {
|
||||
return PracticeSelection({});
|
||||
}
|
||||
final queue = await _fillActivityQueue(eligibleTokens);
|
||||
final queue = await _fillActivityQueue(
|
||||
eligibleTokens,
|
||||
langCode.split('-')[0],
|
||||
);
|
||||
final selection = PracticeSelection(queue);
|
||||
return selection;
|
||||
}
|
||||
|
|
@ -110,10 +113,11 @@ class PracticeSelectionRepo {
|
|||
|
||||
static Future<Map<ActivityTypeEnum, List<PracticeTarget>>> _fillActivityQueue(
|
||||
List<PangeaToken> tokens,
|
||||
String language,
|
||||
) async {
|
||||
final queue = <ActivityTypeEnum, List<PracticeTarget>>{};
|
||||
for (final type in ActivityTypeEnum.practiceTypes) {
|
||||
queue[type] = await _buildActivity(type, tokens);
|
||||
queue[type] = await _buildActivity(type, tokens, language);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
|
@ -147,9 +151,10 @@ class PracticeSelectionRepo {
|
|||
static Future<List<PracticeTarget>> _buildActivity(
|
||||
ActivityTypeEnum activityType,
|
||||
List<PangeaToken> tokens,
|
||||
String language,
|
||||
) async {
|
||||
if (activityType == ActivityTypeEnum.morphId) {
|
||||
return _buildMorphActivity(tokens);
|
||||
return _buildMorphActivity(tokens, language);
|
||||
}
|
||||
|
||||
List<PangeaToken> practiceTokens = List<PangeaToken>.from(tokens);
|
||||
|
|
@ -166,7 +171,11 @@ class PracticeSelectionRepo {
|
|||
return [];
|
||||
}
|
||||
|
||||
final scores = await _fetchPriorityScores(practiceTokens, activityType);
|
||||
final scores = await _fetchPriorityScores(
|
||||
practiceTokens,
|
||||
activityType,
|
||||
language,
|
||||
);
|
||||
|
||||
practiceTokens.sort((a, b) => _sortTokens(a, b, scores[a]!, scores[b]!));
|
||||
practiceTokens = practiceTokens.take(8).toList();
|
||||
|
|
@ -182,12 +191,14 @@ class PracticeSelectionRepo {
|
|||
|
||||
static Future<List<PracticeTarget>> _buildMorphActivity(
|
||||
List<PangeaToken> tokens,
|
||||
String language,
|
||||
) async {
|
||||
final List<PangeaToken> practiceTokens = List<PangeaToken>.from(tokens);
|
||||
final candidates = practiceTokens.expand(_tokenToMorphTargets).toList();
|
||||
final scores = await _fetchPriorityScores(
|
||||
practiceTokens,
|
||||
ActivityTypeEnum.morphId,
|
||||
language,
|
||||
);
|
||||
candidates.sort(
|
||||
(a, b) => _sortMorphTargets(
|
||||
|
|
@ -211,6 +222,7 @@ class PracticeSelectionRepo {
|
|||
static Future<Map<PangeaToken, int>> _fetchPriorityScores(
|
||||
List<PangeaToken> tokens,
|
||||
ActivityTypeEnum activityType,
|
||||
String language,
|
||||
) async {
|
||||
final scores = <PangeaToken, int>{};
|
||||
for (final token in tokens) {
|
||||
|
|
@ -224,7 +236,7 @@ class PracticeSelectionRepo {
|
|||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getConstructUses(ids);
|
||||
.getConstructUses(ids, language);
|
||||
|
||||
for (final token in tokens) {
|
||||
final construct = constructs[idMap[token]];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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/common/utils/error_handler.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';
|
||||
|
|
@ -181,6 +182,20 @@ class PracticeController with ChangeNotifier {
|
|||
|
||||
// we don't take off points for incorrect emoji matches
|
||||
if (_activity is! EmojiPracticeActivityModel || isCorrect) {
|
||||
final l2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort;
|
||||
if (l2 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "User L2 is null when trying to log construct use for token ${token.text.content} in practice activity",
|
||||
data: {
|
||||
"eventId": pangeaMessageEvent.eventId,
|
||||
"token": token.text.content,
|
||||
"activityType": _activity!.activityType.toString(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final constructUseType = PracticeRecordController.lastResponse(
|
||||
_activity!.practiceTarget,
|
||||
)!.useType(_activity!.activityType);
|
||||
|
|
@ -202,7 +217,7 @@ class PracticeController with ChangeNotifier {
|
|||
),
|
||||
];
|
||||
|
||||
updateService.addAnalytics(targetId, constructs);
|
||||
updateService.addAnalytics(targetId, constructs, l2);
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
event.eventId,
|
||||
"word-zoom-card-${token.text.uniqueKey}",
|
||||
token,
|
||||
pangeaMessageEvent.messageDisplayLangCode.split('-').first,
|
||||
Matrix.of(context).analyticsDataService,
|
||||
roomId: event.room.id,
|
||||
eventId: event.eventId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mixin TokenRenderingMixin {
|
|||
String cacheKey,
|
||||
String targetId,
|
||||
PangeaToken token,
|
||||
String language,
|
||||
AnalyticsDataService analyticsService, {
|
||||
String? roomId,
|
||||
String? eventId,
|
||||
|
|
@ -36,7 +37,11 @@ mixin TokenRenderingMixin {
|
|||
),
|
||||
];
|
||||
|
||||
await analyticsService.updateService.addAnalytics(targetId, constructs);
|
||||
await analyticsService.updateService.addAnalytics(
|
||||
targetId,
|
||||
constructs,
|
||||
language,
|
||||
);
|
||||
TokensUtil.clearNewTokenCache();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,11 +213,12 @@ class UserController {
|
|||
// Do not await. This function pulls level from analytics,
|
||||
// so it waits for analytics to finish initializing. Analytics waits for user controller to
|
||||
// finish initializing, so this would cause a deadlock.
|
||||
if (publicProfile!.analytics.isEmpty) {
|
||||
final l2 = userL2;
|
||||
if (publicProfile!.analytics.isEmpty && l2 != null) {
|
||||
final analyticsService =
|
||||
MatrixState.pangeaController.matrixState.analyticsDataService;
|
||||
|
||||
final data = await analyticsService.derivedData;
|
||||
final data = await analyticsService.derivedData(l2.langCodeShort);
|
||||
updateAnalyticsProfile(level: data.level);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue