Merge pull request #930 from pangeachat/analytics-tweaks
some questions, name changes, and a couple switches from grammar to m…
This commit is contained in:
commit
f2b5448fb0
33 changed files with 209 additions and 1579 deletions
|
|
@ -15,8 +15,8 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
|||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
|
|
@ -679,17 +679,16 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
|
||||
if (msgEventId != null) {
|
||||
pangeaController.myAnalytics.setState(
|
||||
pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
roomId: room.id,
|
||||
constructs: [
|
||||
...(choreo!.grammarConstructUses(metadata: metadata)),
|
||||
...(originalSent!.vocabUses(
|
||||
...originalSent!.vocabAndMorphUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent!.tokens,
|
||||
metadata: metadata,
|
||||
)),
|
||||
),
|
||||
],
|
||||
origin: AnalyticsUpdateOrigin.sendMessage,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/pinned_events.dart';
|
|||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
|
||||
|
|
|
|||
|
|
@ -1016,8 +1016,6 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
MatrixState.pangeaController.myAnalytics.initialize();
|
||||
MatrixState.pangeaController.analytics.initialize();
|
||||
await _initPangeaControllers(client);
|
||||
// Pangea#
|
||||
if (!mounted) return;
|
||||
|
|
@ -1028,6 +1026,8 @@ class ChatListController extends State<ChatList>
|
|||
|
||||
// #Pangea
|
||||
Future<void> _initPangeaControllers(Client client) async {
|
||||
MatrixState.pangeaController.putAnalytics.initialize();
|
||||
MatrixState.pangeaController.getAnalytics.initialize();
|
||||
if (mounted) {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
|
@ -318,7 +318,7 @@ class ITController {
|
|||
.toList();
|
||||
|
||||
// Save those choices' tokens to local construct analytics as ignored tokens
|
||||
choreographer.pangeaController.myAnalytics.addDraftUses(
|
||||
choreographer.pangeaController.putAnalytics.addDraftUses(
|
||||
ignoredTokens ?? [],
|
||||
choreographer.roomId,
|
||||
ConstructUseTypeEnum.ignIt,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -369,7 +369,7 @@ class ITChoices extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
if (!continuance.wasClicked) {
|
||||
controller.choreographer.pangeaController.myAnalytics.addDraftUses(
|
||||
controller.choreographer.pangeaController.putAnalytics.addDraftUses(
|
||||
continuance.tokens,
|
||||
controller.choreographer.roomId,
|
||||
continuance.level > 1
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import 'dart:math';
|
|||
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
|
@ -70,10 +69,10 @@ class GetAnalyticsController {
|
|||
|
||||
void initialize() {
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.myAnalytics.analyticsUpdateStream.stream
|
||||
.putAnalytics.analyticsUpdateStream.stream
|
||||
.listen(onAnalyticsUpdate);
|
||||
|
||||
_pangeaController.myAnalytics.lastUpdatedCompleter.future.then((_) {
|
||||
_pangeaController.putAnalytics.lastUpdatedCompleter.future.then((_) {
|
||||
getConstructs().then((_) => updateAnalyticsStream());
|
||||
});
|
||||
}
|
||||
|
|
@ -127,11 +126,12 @@ class GetAnalyticsController {
|
|||
uses: constructs,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
);
|
||||
final errors = ConstructListModel(
|
||||
|
||||
final morphs = ConstructListModel(
|
||||
uses: constructs,
|
||||
type: ConstructTypeEnum.grammar,
|
||||
type: ConstructTypeEnum.morph,
|
||||
);
|
||||
return words.points + errors.points;
|
||||
return words.points + morphs.points;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get allConstructUses {
|
||||
|
|
@ -168,7 +168,7 @@ class GetAnalyticsController {
|
|||
return formattedCache;
|
||||
} catch (err) {
|
||||
// if something goes wrong while trying to format the local data, clear it
|
||||
_pangeaController.myAnalytics
|
||||
_pangeaController.putAnalytics
|
||||
.clearMessagesSinceUpdate(clearDrafts: true);
|
||||
return {};
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ class GetAnalyticsController {
|
|||
await client.roomsLoading;
|
||||
|
||||
// don't try to get constructs until last updated time has been loaded
|
||||
await _pangeaController.myAnalytics.lastUpdatedCompleter.future;
|
||||
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
|
||||
|
||||
// if forcing a refreshing, clear the cache
|
||||
if (forceUpdate) _cache.clear();
|
||||
|
|
@ -226,25 +226,20 @@ class GetAnalyticsController {
|
|||
final List<ConstructAnalyticsEvent> constructEvents =
|
||||
await allMyConstructs();
|
||||
|
||||
final List<OneConstructUse> unfilteredUses = [];
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final event in constructEvents) {
|
||||
unfilteredUses.addAll(event.content.uses);
|
||||
uses.addAll(event.content.uses);
|
||||
}
|
||||
|
||||
// filter out any constructs that are not relevant to the user
|
||||
final List<OneConstructUse> filteredUses = await filterConstructs(
|
||||
unfilteredConstructs: unfilteredUses,
|
||||
);
|
||||
|
||||
// if there isn't already a valid, local cache, cache the filtered uses
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
constructType: constructType,
|
||||
uses: filteredUses,
|
||||
uses: uses,
|
||||
);
|
||||
}
|
||||
|
||||
return filteredUses;
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// Get the last time the user updated their analytics for their current l2
|
||||
|
|
@ -271,21 +266,6 @@ class GetAnalyticsController {
|
|||
return await analyticsRoom.getAnalyticsEvents(userId: client.userID!) ?? [];
|
||||
}
|
||||
|
||||
/// Filter out constructs that are not relevant to the user, specifically those from
|
||||
/// rooms in which the user is a teacher and those that are interative translation span constructs
|
||||
Future<List<OneConstructUse>> filterConstructs({
|
||||
required List<OneConstructUse> unfilteredConstructs,
|
||||
}) async {
|
||||
return unfilteredConstructs
|
||||
.where(
|
||||
(use) =>
|
||||
use.lemma != "Try interactive translation" &&
|
||||
use.lemma != "itStart" ||
|
||||
use.lemma != MatchRuleIds.interactiveTranslation,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Get the cached construct uses for the current user, if it exists
|
||||
List<OneConstructUse>? getConstructsLocal({
|
||||
ConstructTypeEnum? constructType,
|
||||
|
|
@ -295,7 +275,7 @@ class GetAnalyticsController {
|
|||
);
|
||||
|
||||
if (index > -1) {
|
||||
final DateTime? lastUpdated = _pangeaController.myAnalytics.lastUpdated;
|
||||
final DateTime? lastUpdated = _pangeaController.putAnalytics.lastUpdated;
|
||||
if (_cache[index].needsUpdate(lastUpdated)) {
|
||||
_cache.removeAt(index);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,535 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/class_default_values.dart';
|
||||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'base_controller.dart';
|
||||
import 'pangea_controller.dart';
|
||||
|
||||
// controls the fetching of analytics data
|
||||
class AnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
final List<ConstructCacheEntry> _cachedConstructs = [];
|
||||
|
||||
AnalyticsController(PangeaController pangeaController) : super() {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
String get langCode =>
|
||||
_pangeaController.languageController.userL2?.langCode ??
|
||||
_pangeaController.pLanguageStore.targetOptions.first.langCode;
|
||||
|
||||
// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
|
||||
|
||||
// TimeSpan get currentAnalyticsTimeSpan {
|
||||
// try {
|
||||
// final String? str = _pangeaController.pStoreService.read(
|
||||
// _analyticsTimeSpanKey,
|
||||
// );
|
||||
// return str != null
|
||||
// ? TimeSpan.values.firstWhere((e) {
|
||||
// final spanString = e.toString();
|
||||
// return spanString == str;
|
||||
// })
|
||||
// : ClassDefaultValues.defaultTimeSpan;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return ClassDefaultValues.defaultTimeSpan;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
|
||||
// await _pangeaController.pStoreService.save(
|
||||
// _analyticsTimeSpanKey,
|
||||
// timeSpan.toString(),
|
||||
// );
|
||||
// setState();
|
||||
// }
|
||||
|
||||
// String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
|
||||
|
||||
// LanguageModel get currentAnalyticsLang {
|
||||
// try {
|
||||
// final String? str = _pangeaController.pStoreService.read(
|
||||
// _analyticsSpaceLangKey,
|
||||
// );
|
||||
// return str != null
|
||||
// ? PangeaLanguage.byLangCode(str)
|
||||
// : _pangeaController.languageController.userL2 ??
|
||||
// _pangeaController.pLanguageStore.targetOptions.first;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return _pangeaController.pLanguageStore.targetOptions.first;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
|
||||
// await _pangeaController.pStoreService.save(
|
||||
// _analyticsSpaceLangKey,
|
||||
// lang.langCode,
|
||||
// );
|
||||
// setState();
|
||||
// }
|
||||
|
||||
/// Get the last time the user updated their analytics.
|
||||
/// Tries to get the last time the user updated analytics for their current L2.
|
||||
/// If there isn't yet an analytics room reacted for their L2, checks if the
|
||||
/// user has any other analytics rooms and returns the most recent update time.
|
||||
Future<DateTime?> myAnalyticsLastUpdated() async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
final Map<String, DateTime> langCodeLastUpdates = {};
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final String? roomLang = analyticsRoom.madeForLang;
|
||||
if (roomLang == null) continue;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
if (lastUpdated != null) {
|
||||
langCodeLastUpdates[roomLang] = lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
if (langCodeLastUpdates.isEmpty) return null;
|
||||
final String? l2Code =
|
||||
_pangeaController.languageController.userL2?.langCode;
|
||||
if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) {
|
||||
return langCodeLastUpdates[l2Code];
|
||||
}
|
||||
return langCodeLastUpdates.values.reduce(
|
||||
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
|
||||
);
|
||||
}
|
||||
|
||||
/// check if any students have recently updated their analytics
|
||||
/// if any have, then the cache needs to be updated
|
||||
Future<DateTime?> spaceAnalyticsLastUpdated(
|
||||
Room space,
|
||||
) async {
|
||||
await space.requestParticipants();
|
||||
|
||||
final List<Future<DateTime?>> lastUpdatedFutures = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom == null) continue;
|
||||
lastUpdatedFutures.add(
|
||||
analyticsRoom.analyticsLastUpdated(student.id),
|
||||
);
|
||||
}
|
||||
|
||||
final List<DateTime?> lastUpdatedWithNulls =
|
||||
await Future.wait(lastUpdatedFutures);
|
||||
final List<DateTime> lastUpdates =
|
||||
lastUpdatedWithNulls.where((e) => e != null).cast<DateTime>().toList();
|
||||
if (lastUpdates.isNotEmpty) {
|
||||
return lastUpdates.reduce(
|
||||
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs(
|
||||
TimeSpan timeSpan,
|
||||
) async {
|
||||
final Room? analyticsRoom =
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
since: timeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
final List<ConstructAnalyticsEvent> allConstructs = roomEvents ?? [];
|
||||
|
||||
return allConstructs
|
||||
.where((construct) => construct.content.uses.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
|
||||
Room space,
|
||||
TimeSpan timeSpan,
|
||||
) async {
|
||||
await space.requestParticipants();
|
||||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom != null) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
since: timeSpan.cutOffDate,
|
||||
userId: student.id,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
constructEvents.addAll(roomEvents ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> spaceChildrenIds = space.allSpaceChildRoomIds;
|
||||
final List<ConstructAnalyticsEvent> allConstructs = [];
|
||||
for (final constructEvent in constructEvents) {
|
||||
constructEvent.content.uses.removeWhere(
|
||||
(use) => !spaceChildrenIds.contains(use.chatId),
|
||||
);
|
||||
|
||||
if (constructEvent.content.uses.isNotEmpty) {
|
||||
allConstructs.add(constructEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return allConstructs;
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent> filterStudentConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
String? studentId,
|
||||
) {
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
filtered.removeWhere((element) => element.event.senderId != studentId);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent> filterRoomConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
String? roomID,
|
||||
) {
|
||||
final List<ConstructAnalyticsEvent> filtered = [...unfilteredConstructs];
|
||||
for (final construct in filtered) {
|
||||
construct.content.uses.removeWhere((u) => u.chatId != roomID);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> filterPrivateChatConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) async {
|
||||
final List<String> privateChatIds = space.allSpaceChildRoomIds;
|
||||
final resp = await space.client.getSpaceHierarchy(space.id);
|
||||
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
for (final id in chatIds) {
|
||||
privateChatIds.removeWhere((e) => e == id);
|
||||
}
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
for (final construct in filtered) {
|
||||
construct.content.uses.removeWhere(
|
||||
(use) => !privateChatIds.contains(use.chatId),
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> filterSpaceConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) async {
|
||||
final resp = await space.client.getSpaceHierarchy(space.id);
|
||||
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
|
||||
for (final construct in filtered) {
|
||||
construct.content.uses.removeWhere(
|
||||
(use) => !chatIds.contains(use.chatId),
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
DateTime? lastUpdated,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final index = _cachedConstructs.indexWhere(
|
||||
(e) =>
|
||||
e.timeSpan == timeSpan &&
|
||||
e.type == constructType &&
|
||||
e.defaultSelected.id == defaultSelected.id &&
|
||||
e.defaultSelected.type == defaultSelected.type &&
|
||||
e.selected?.id == selected?.id &&
|
||||
e.selected?.type == selected?.type &&
|
||||
e.langCode == langCode,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
if (_cachedConstructs[index].needsUpdate(lastUpdated)) {
|
||||
_cachedConstructs.removeAt(index);
|
||||
return null;
|
||||
}
|
||||
return _cachedConstructs[index].events;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void cacheConstructs({
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final entry = ConstructCacheEntry(
|
||||
timeSpan: timeSpan,
|
||||
type: constructType,
|
||||
events: List.from(events),
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: langCode,
|
||||
);
|
||||
_cachedConstructs.add(entry);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
ConstructTypeEnum? constructType,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allMyConstructs(timeSpan);
|
||||
|
||||
final Room? space = selected?.type == AnalyticsEntryType.space
|
||||
? _pangeaController.matrixState.client.getRoomById(selected!.id)
|
||||
: null;
|
||||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allSpaceMemberConstructs(
|
||||
space,
|
||||
timeSpan,
|
||||
);
|
||||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> filterConstructs({
|
||||
required List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
if ([AnalyticsEntryType.privateChats, AnalyticsEntryType.space]
|
||||
.contains(selected?.type)) {
|
||||
assert(space != null);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unfilteredConstructs.length; i++) {
|
||||
final construct = unfilteredConstructs[i];
|
||||
construct.content.uses.removeWhere(
|
||||
(use) => use.timeStamp.isBefore(timeSpan.cutOffDate),
|
||||
);
|
||||
}
|
||||
|
||||
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
|
||||
|
||||
switch (selected?.type) {
|
||||
case null:
|
||||
return unfilteredConstructs;
|
||||
case AnalyticsEntryType.student:
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
throw Exception(
|
||||
"student filtering not available for default filter ${defaultSelected.type}",
|
||||
);
|
||||
}
|
||||
return filterStudentConstructs(unfilteredConstructs, selected!.id);
|
||||
case AnalyticsEntryType.room:
|
||||
return filterRoomConstructs(unfilteredConstructs, selected?.id);
|
||||
case AnalyticsEntryType.privateChats:
|
||||
return defaultSelected.type == AnalyticsEntryType.student
|
||||
? throw "private chat filtering not available for my analytics"
|
||||
: await filterPrivateChatConstructs(unfilteredConstructs, space!);
|
||||
case AnalyticsEntryType.space:
|
||||
return await filterSpaceConstructs(unfilteredConstructs, space!);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = true,
|
||||
bool forceUpdate = false,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
debugPrint("getting constructs");
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
|
||||
Room? space;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
);
|
||||
if (space == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "space not found in setConstructs",
|
||||
data: {
|
||||
"defaultSelected": defaultSelected,
|
||||
"selected": selected,
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
// if default selected view is my analytics, check for the last
|
||||
// time the logged in user updated their analytics events
|
||||
// this gets passed to getAnalyticsLocal to determine if the cached
|
||||
// entry is out-of-date
|
||||
lastUpdated = await myAnalyticsLastUpdated();
|
||||
} else {
|
||||
// else, get the last time a student in the space updated their analytics
|
||||
lastUpdated = await spaceAnalyticsLastUpdated(
|
||||
space!,
|
||||
);
|
||||
}
|
||||
|
||||
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
|
||||
timeSpan: timeSpan,
|
||||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
debugPrint("returning local constructs");
|
||||
return local;
|
||||
}
|
||||
debugPrint("fetching new constructs");
|
||||
|
||||
final filteredConstructs = space == null
|
||||
? await getMyConstructs(
|
||||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
)
|
||||
: await getSpaceConstructs(
|
||||
constructType: constructType,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
|
||||
if (removeIT) {
|
||||
for (final construct in filteredConstructs) {
|
||||
construct.content.uses.removeWhere(
|
||||
(element) =>
|
||||
element.lemma == "Try interactive translation" ||
|
||||
element.lemma == "itStart" ||
|
||||
element.lemma == MatchRuleIds.interactiveTranslation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
constructType: constructType,
|
||||
events: filteredConstructs,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
return filteredConstructs;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CacheEntry {
|
||||
final String langCode;
|
||||
final TimeSpan timeSpan;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
AnalyticsSelected? selected;
|
||||
late final DateTime _createdAt;
|
||||
|
||||
CacheEntry({
|
||||
required this.timeSpan,
|
||||
required this.defaultSelected,
|
||||
required this.langCode,
|
||||
this.selected,
|
||||
}) {
|
||||
_createdAt = DateTime.now();
|
||||
}
|
||||
|
||||
bool get isExpired =>
|
||||
DateTime.now().difference(_createdAt).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
|
||||
|
||||
bool needsUpdate(DateTime? lastEventUpdated) {
|
||||
// cache entry is invalid if it's older than the last event update
|
||||
// if lastEventUpdated is null, that would indicate that no events
|
||||
// of this type have been sent to the room. In this case, there
|
||||
// shouldn't be any cached data.
|
||||
if (lastEventUpdated == null) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(message: "lastEventUpdated is null in needsUpdate"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return _createdAt.isBefore(lastEventUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructCacheEntry extends CacheEntry {
|
||||
final ConstructTypeEnum? type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
|
||||
ConstructCacheEntry({
|
||||
required this.events,
|
||||
required super.timeSpan,
|
||||
required super.langCode,
|
||||
required super.defaultSelected,
|
||||
this.type,
|
||||
super.selected,
|
||||
});
|
||||
}
|
||||
|
|
@ -12,10 +12,10 @@ import 'package:fluffychat/pangea/controllers/language_controller.dart';
|
|||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
|
|
@ -44,11 +44,12 @@ class PangeaController {
|
|||
late LanguageController languageController;
|
||||
late ClassController classController;
|
||||
late PermissionsController permissionsController;
|
||||
// late AnalyticsController analytics;
|
||||
late GetAnalyticsController analytics;
|
||||
late MyAnalyticsController myAnalytics;
|
||||
late GetAnalyticsController getAnalytics;
|
||||
late PutAnalyticsController putAnalytics;
|
||||
late WordController wordNet;
|
||||
late MessageDataController messageData;
|
||||
|
||||
// TODO: make these static so we can remove from here
|
||||
late ContextualDefinitionController definitions;
|
||||
late ITFeedbackController itFeedback;
|
||||
late InstructionsController instructions;
|
||||
|
|
@ -93,9 +94,8 @@ class PangeaController {
|
|||
languageController = LanguageController(this);
|
||||
classController = ClassController(this);
|
||||
permissionsController = PermissionsController(this);
|
||||
// analytics = AnalyticsController(this);
|
||||
analytics = GetAnalyticsController(this);
|
||||
myAnalytics = MyAnalyticsController(this);
|
||||
getAnalytics = GetAnalyticsController(this);
|
||||
putAnalytics = PutAnalyticsController(this);
|
||||
messageData = MessageDataController(this);
|
||||
wordNet = WordController(this);
|
||||
definitions = ContextualDefinitionController(this);
|
||||
|
|
@ -146,13 +146,13 @@ class PangeaController {
|
|||
case LoginState.loggedOut:
|
||||
case LoginState.softLoggedOut:
|
||||
// Reset cached analytics data
|
||||
MatrixState.pangeaController.myAnalytics.dispose();
|
||||
MatrixState.pangeaController.analytics.dispose();
|
||||
MatrixState.pangeaController.putAnalytics.dispose();
|
||||
MatrixState.pangeaController.getAnalytics.dispose();
|
||||
break;
|
||||
case LoginState.loggedIn:
|
||||
// Initialize analytics data
|
||||
MatrixState.pangeaController.myAnalytics.initialize();
|
||||
MatrixState.pangeaController.analytics.initialize();
|
||||
MatrixState.pangeaController.putAnalytics.initialize();
|
||||
MatrixState.pangeaController.getAnalytics.initialize();
|
||||
break;
|
||||
}
|
||||
if (state != LoginState.loggedIn) {
|
||||
|
|
@ -169,27 +169,12 @@ class PangeaController {
|
|||
GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID);
|
||||
}
|
||||
|
||||
// void startChatWithBotIfNotPresent() {
|
||||
// Future.delayed(const Duration(milliseconds: 5000), () async {
|
||||
// try {
|
||||
// if (pStoreService.read("started_bot_chat", addClientIdToKey: false) ??
|
||||
// false) {
|
||||
// return;
|
||||
// }
|
||||
// await pStoreService.save("started_bot_chat", true,
|
||||
// addClientIdToKey: false);
|
||||
// final rooms = matrixState.client.rooms;
|
||||
|
||||
// await matrixState.client.startDirectChat(
|
||||
// BotName.byEnvironment,
|
||||
// enableEncryption: false,
|
||||
// );
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: stack);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
Future<void> resetAnalytics() async {
|
||||
putAnalytics.dispose();
|
||||
getAnalytics.dispose();
|
||||
putAnalytics.initialize();
|
||||
getAnalytics.initialize();
|
||||
}
|
||||
|
||||
void startChatWithBotIfNotPresent() {
|
||||
Future.delayed(const Duration(milliseconds: 10000), () async {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ 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 MyAnalyticsController extends BaseController<AnalyticsStream> {
|
||||
class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
||||
late PangeaController _pangeaController;
|
||||
CachedStreamController<AnalyticsUpdate> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdate>();
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
StreamSubscription? _languageStream;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
|
@ -47,16 +48,25 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
/// the time since the last update that will trigger an automatic update
|
||||
final Duration _timeSinceUpdate = const Duration(days: 1);
|
||||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
PutAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
// Listen to a stream that provides the eventIDs
|
||||
// of new messages sent by the logged in user
|
||||
// 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.stateStream.listen((update) {
|
||||
if (update is Map<String, dynamic>) {
|
||||
final previousL2 = update['prev_target_lang'];
|
||||
_onUpdateLanguages(previousL2);
|
||||
}
|
||||
});
|
||||
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +78,8 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
lastUpdatedCompleter = Completer<DateTime?>();
|
||||
_analyticsStream?.cancel();
|
||||
_analyticsStream = null;
|
||||
_languageStream?.cancel();
|
||||
_languageStream = null;
|
||||
_refreshAnalyticsIfOutdated();
|
||||
clearMessagesSinceUpdate();
|
||||
}
|
||||
|
|
@ -79,7 +91,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
try {
|
||||
// if lastUpdated hasn't been set yet, set it
|
||||
lastUpdated ??=
|
||||
await _pangeaController.analytics.myAnalyticsLastUpdated();
|
||||
await _pangeaController.getAnalytics.myAnalyticsLastUpdated();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
s: s,
|
||||
|
|
@ -100,8 +112,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Given the data from a newly sent message, format and cache
|
||||
/// the message's construct data locally and reset the update timer
|
||||
/// 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 List<OneConstructUse> constructs = _getDraftUses(data.roomId);
|
||||
|
||||
|
|
@ -110,25 +123,31 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
final String eventID = data.eventId;
|
||||
final String roomID = data.roomId;
|
||||
|
||||
_pangeaController.analytics
|
||||
.filterConstructs(unfilteredConstructs: constructs)
|
||||
.then((filtered) {
|
||||
for (final use in filtered) {
|
||||
if (kDebugMode) {
|
||||
for (final use in constructs) {
|
||||
debugPrint(
|
||||
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
|
||||
);
|
||||
}
|
||||
if (filtered.isEmpty) return;
|
||||
}
|
||||
|
||||
final level = _pangeaController.analytics.level;
|
||||
if (constructs.isEmpty) return;
|
||||
|
||||
_addLocalMessage(eventID, filtered).then(
|
||||
(_) {
|
||||
_clearDraftUses(roomID);
|
||||
_decideWhetherToUpdateAnalyticsRoom(level, data.origin);
|
||||
},
|
||||
);
|
||||
});
|
||||
final level = _pangeaController.getAnalytics.level;
|
||||
|
||||
_addLocalMessage(eventID, constructs).then(
|
||||
(_) {
|
||||
_clearDraftUses(roomID);
|
||||
_decideWhetherToUpdateAnalyticsRoom(level, data.origin);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateLanguages(String previousL2) async {
|
||||
await sendLocalAnalyticsToAnalyticsRoom(
|
||||
l2Override: previousL2,
|
||||
);
|
||||
_pangeaController.resetAnalytics();
|
||||
}
|
||||
|
||||
void addDraftUses(
|
||||
|
|
@ -142,8 +161,12 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final uses = tokens
|
||||
.where((token) => token.lemma.saveVocab)
|
||||
// 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,
|
||||
|
|
@ -155,7 +178,8 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
)
|
||||
.toList();
|
||||
|
||||
for (final token in tokens) {
|
||||
// get all our grammar constructs
|
||||
for (final token in tokensToSave) {
|
||||
for (final entry in token.morph.entries) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
|
|
@ -177,19 +201,19 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
}
|
||||
}
|
||||
|
||||
final level = _pangeaController.analytics.level;
|
||||
final level = _pangeaController.getAnalytics.level;
|
||||
_addLocalMessage('draft$roomID', uses).then(
|
||||
(_) => _decideWhetherToUpdateAnalyticsRoom(level, origin),
|
||||
);
|
||||
}
|
||||
|
||||
List<OneConstructUse> _getDraftUses(String roomID) {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
|
||||
return currentCache['draft$roomID'] ?? [];
|
||||
}
|
||||
|
||||
void _clearDraftUses(String roomID) {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
|
||||
currentCache.remove('draft$roomID');
|
||||
_setMessagesSinceUpdate(currentCache);
|
||||
}
|
||||
|
|
@ -201,7 +225,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
List<OneConstructUse> constructs,
|
||||
) async {
|
||||
try {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
|
||||
constructs.addAll(currentCache[cacheKey] ?? []);
|
||||
currentCache[cacheKey] = constructs;
|
||||
|
||||
|
|
@ -231,14 +255,14 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
sendLocalAnalyticsToAnalyticsRoom();
|
||||
});
|
||||
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.length >
|
||||
if (_pangeaController.getAnalytics.messagesSinceUpdate.length >
|
||||
_maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
sendLocalAnalyticsToAnalyticsRoom();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newLevel = _pangeaController.analytics.level;
|
||||
final int newLevel = _pangeaController.getAnalytics.level;
|
||||
newLevel > prevLevel
|
||||
? sendLocalAnalyticsToAnalyticsRoom()
|
||||
: analyticsUpdateStream.add(
|
||||
|
|
@ -253,7 +277,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
return;
|
||||
}
|
||||
|
||||
final localCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final localCache = _pangeaController.getAnalytics.messagesSinceUpdate;
|
||||
final draftKeys = localCache.keys.where((key) => key.startsWith('draft'));
|
||||
if (draftKeys.isEmpty) {
|
||||
_pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate);
|
||||
|
|
@ -294,6 +318,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
/// since the last update and notifies the [analyticsUpdateStream].
|
||||
Future<void> sendLocalAnalyticsToAnalyticsRoom({
|
||||
onLogout = false,
|
||||
String? l2Override,
|
||||
}) async {
|
||||
if (_pangeaController.matrixState.client.userID == null) return;
|
||||
if (!(_updateCompleter?.isCompleted ?? true)) {
|
||||
|
|
@ -302,7 +327,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
}
|
||||
_updateCompleter = Completer<void>();
|
||||
try {
|
||||
await _updateAnalytics();
|
||||
await _updateAnalytics(l2Override: l2Override);
|
||||
clearMessagesSinceUpdate();
|
||||
|
||||
lastUpdated = DateTime.now();
|
||||
|
|
@ -326,22 +351,23 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
|
||||
/// Updates the analytics by sending cached analytics data to the analytics room.
|
||||
/// The analytics room is determined based on the user's current target language.
|
||||
Future<void> _updateAnalytics() async {
|
||||
Future<void> _updateAnalytics({String? l2Override}) async {
|
||||
// if there's no cached construct data, there's nothing to send
|
||||
final cachedConstructs = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final cachedConstructs = _pangeaController.getAnalytics.messagesSinceUpdate;
|
||||
final bool onlyDraft = cachedConstructs.length == 1 &&
|
||||
cachedConstructs.keys.single.startsWith('draft');
|
||||
if (cachedConstructs.isEmpty || onlyDraft) return;
|
||||
|
||||
// if missing important info, don't send analytics. Could happen if user just signed up.
|
||||
if (userL2 == null || _client.userID == null) return;
|
||||
final l2Code = l2Override ?? userL2;
|
||||
if (l2Code == null || _client.userID == null) return;
|
||||
|
||||
// analytics room for the user and current target language
|
||||
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
|
||||
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(l2Code);
|
||||
|
||||
// and send cached analytics data to the room
|
||||
await analyticsRoom?.sendConstructsEvent(
|
||||
_pangeaController.analytics.locallyCachedSentConstructs,
|
||||
_pangeaController.getAnalytics.locallyCachedSentConstructs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -73,8 +73,14 @@ class UserController extends BaseController {
|
|||
Profile Function(Profile) update, {
|
||||
waitForDataInSync = false,
|
||||
}) async {
|
||||
final prevTargetLang = profile.userSettings.targetLanguage;
|
||||
|
||||
final Profile updatedProfile = update(profile);
|
||||
await updatedProfile.saveProfileData(waitForDataInSync: waitForDataInSync);
|
||||
|
||||
if (prevTargetLang != updatedProfile.userSettings.targetLanguage) {
|
||||
setState({'prev_target_lang': prevTargetLang});
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new profile for the user with the given date of birth.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum ConstructTypeEnum {
|
||||
grammar,
|
||||
/// for vocabulary words
|
||||
vocab,
|
||||
|
||||
/// for morphs, actually called "Grammar" in the UI... :P
|
||||
morph,
|
||||
}
|
||||
|
||||
extension ConstructExtension on ConstructTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.grammar:
|
||||
return 'grammar';
|
||||
case ConstructTypeEnum.vocab:
|
||||
return 'vocab';
|
||||
case ConstructTypeEnum.morph:
|
||||
|
|
@ -20,8 +23,6 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
|
||||
int get maxXPPerLemma {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.grammar:
|
||||
return 0;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return AnalyticsConstants.vocabUseMaxXP;
|
||||
case ConstructTypeEnum.morph:
|
||||
|
|
@ -33,9 +34,6 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
class ConstructTypeUtil {
|
||||
static ConstructTypeEnum fromString(String? string) {
|
||||
switch (string) {
|
||||
case 'g':
|
||||
case 'grammar':
|
||||
return ConstructTypeEnum.grammar;
|
||||
case 'v':
|
||||
case 'vocab':
|
||||
return ConstructTypeEnum.vocab;
|
||||
|
|
@ -43,6 +41,7 @@ class ConstructTypeUtil {
|
|||
case 'morph':
|
||||
return ConstructTypeEnum.morph;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return ConstructTypeEnum.vocab;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corWL:
|
||||
return 3;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return 2;
|
||||
|
||||
|
|
@ -115,6 +114,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.nan:
|
||||
return 0;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return -1;
|
||||
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
return -2;
|
||||
|
|
|
|||
|
|
@ -37,13 +37,13 @@ import '../../models/choreo_record.dart';
|
|||
import '../../models/representation_content_model.dart';
|
||||
import '../client_extension/client_extension.dart';
|
||||
|
||||
part "children_and_parents_extension.dart";
|
||||
part "events_extension.dart";
|
||||
part "room_analytics_extension.dart";
|
||||
part "room_children_and_parents_extension.dart";
|
||||
part "room_events_extension.dart";
|
||||
part "room_information_extension.dart";
|
||||
part "room_settings_extension.dart";
|
||||
part "space_settings_extension.dart";
|
||||
part "user_permissions_extension.dart";
|
||||
part "room_space_settings_extension.dart";
|
||||
part "room_user_permissions_extension.dart";
|
||||
|
||||
extension PangeaRoom on Room {
|
||||
// analytics
|
||||
|
|
|
|||
|
|
@ -282,70 +282,6 @@ extension EventsRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
// ConstructEvent? _vocabEventLocal(String lemma) {
|
||||
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
|
||||
// final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma);
|
||||
|
||||
// return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null;
|
||||
// }
|
||||
|
||||
// Future<ConstructEvent> _vocabEvent(
|
||||
// String lemma,
|
||||
// ConstructType type, [
|
||||
// bool makeIfNull = false,
|
||||
// ]) async {
|
||||
// try {
|
||||
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
|
||||
// ConstructEvent? localEvent = _vocabEventLocal(lemma);
|
||||
|
||||
// if (localEvent != null) return localEvent;
|
||||
|
||||
// await postLoad();
|
||||
// localEvent = _vocabEventLocal(lemma);
|
||||
|
||||
// if (localEvent == null && isRoomOwner && makeIfNull) {
|
||||
// final Event matrixEvent = await _createVocabEvent(lemma, type);
|
||||
// localEvent = ConstructEvent(event: matrixEvent);
|
||||
// }
|
||||
|
||||
// return localEvent!;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<Event> _createVocabEvent(String lemma, ConstructType type) async {
|
||||
// try {
|
||||
// if (!isRoomOwner) {
|
||||
// throw Exception(
|
||||
// "Tried to create vocab event in room where user is not owner",
|
||||
// );
|
||||
// }
|
||||
// final String eventId = await client.setRoomStateWithKey(
|
||||
// id,
|
||||
// PangeaEventTypes.vocab,
|
||||
// lemma,
|
||||
// ConstructUses(lemma: lemma, type: type).toJson(),
|
||||
// );
|
||||
// final Event? event = await getEventById(eventId);
|
||||
|
||||
// if (event == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "null event after creation with eventId $eventId in _createVocabEvent",
|
||||
// );
|
||||
// }
|
||||
// return event;
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Get a list of events in the room that are of type [PangeaEventTypes.construct]
|
||||
/// and have the sender as [userID]. If [count] is provided, the function will
|
||||
/// return at most [count] events.
|
||||
|
|
@ -7,7 +7,6 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
|||
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
|
|
@ -639,34 +638,4 @@ class PangeaMessageEvent {
|
|||
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
|
||||
|
||||
/// all construct uses for the message, including vocab and grammar
|
||||
List<OneConstructUse> get allConstructUses => [
|
||||
..._grammarConstructUses,
|
||||
..._vocabUses,
|
||||
];
|
||||
|
||||
/// get construct uses of type vocab for the message
|
||||
List<OneConstructUse> get _vocabUses {
|
||||
if (originalSent?.tokens != null) {
|
||||
return originalSent!.content.vocabUses(
|
||||
event: event,
|
||||
choreo: originalSent!.choreo,
|
||||
tokens: originalSent!.tokens!,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// get construct uses of type grammar for the message
|
||||
List<OneConstructUse> get _grammarConstructUses =>
|
||||
originalSent?.choreo?.grammarConstructUses(event: event) ?? [];
|
||||
}
|
||||
|
||||
class URLFinder {
|
||||
static Iterable<Match> getMatches(String text) {
|
||||
final RegExp exp =
|
||||
RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
return exp.allMatches(text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,53 +18,29 @@ class ConstructAnalyticsModel {
|
|||
|
||||
factory ConstructAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
if (json[_usesKey] is List) {
|
||||
// This is the new format
|
||||
uses.addAll(
|
||||
(json[_usesKey] as List)
|
||||
.map((use) => OneConstructUse.fromJson(use))
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
// This is the old format. No data on production should be
|
||||
// structured this way, but it's useful for testing.
|
||||
try {
|
||||
final useValues = (json[_usesKey] as Map<String, dynamic>).values;
|
||||
for (final useValue in useValues) {
|
||||
final lemma = useValue['lemma'];
|
||||
final lemmaUses = useValue[_usesKey];
|
||||
for (final useData in lemmaUses) {
|
||||
final use = OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
lemma: lemma,
|
||||
form: useData["form"],
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
metadata: ConstructUseMetaData(
|
||||
eventId: useData["msgId"],
|
||||
roomId: useData["chatId"],
|
||||
timeStamp: DateTime.parse(useData["timeStamp"]),
|
||||
),
|
||||
);
|
||||
uses.add(use);
|
||||
}
|
||||
for (final useJson in json[_usesKey]) {
|
||||
// grammar construct uses are deprecated so but some are saved
|
||||
// here we're filtering from data
|
||||
if (["grammar", "g"].contains(useJson['constructType'])) {
|
||||
continue;
|
||||
} else {
|
||||
uses.add(OneConstructUse.fromJson(useJson));
|
||||
}
|
||||
} catch (err, s) {
|
||||
debugPrint("Error parsing ConstructAnalyticsModel");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
m: "Error parsing ConstructAnalyticsModel",
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(m: "Analytics room with non-list uses");
|
||||
}
|
||||
|
||||
return ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
_usesKey: uses.map((use) => use.toJson()).toList(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'it_step.dart';
|
||||
|
||||
|
|
@ -115,44 +111,6 @@ class ChoreoRecord {
|
|||
|
||||
String get finalMessage =>
|
||||
choreoSteps.isNotEmpty ? choreoSteps.last.text : "";
|
||||
|
||||
/// Get construct uses of type grammar for the message from this ChoreoRecord.
|
||||
/// Takes either an event (typically when the Representation itself is
|
||||
/// available) or construct use metadata (when the event is not available,
|
||||
/// i.e. immediately after message send) to create the construct uses.
|
||||
List<OneConstructUse> grammarConstructUses({
|
||||
Event? event,
|
||||
ConstructUseMetaData? metadata,
|
||||
}) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
if (event?.roomId == null && metadata?.roomId == null) {
|
||||
return uses;
|
||||
}
|
||||
metadata ??= ConstructUseMetaData(
|
||||
roomId: event!.roomId!,
|
||||
eventId: event.eventId,
|
||||
timeStamp: event.originServerTs,
|
||||
);
|
||||
|
||||
for (final step in choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
lemma: name,
|
||||
form: name,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
id: "${metadata.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
/// A new ChoreoRecordStep is saved in the following cases:
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class PangeaMatch {
|
|||
}
|
||||
|
||||
bool isOffsetInMatchSpan(int offset) =>
|
||||
offset >= match.offset && offset <= match.offset + match.length;
|
||||
offset >= match.offset && offset < match.offset + match.length;
|
||||
|
||||
Color get underlineColor {
|
||||
switch (match.rule?.id ?? "unknown") {
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ class PangeaRepresentation {
|
|||
return data;
|
||||
}
|
||||
|
||||
/// Get construct uses of type vocab for the message.
|
||||
/// Get construct uses for the message that weren't captured during language assistance.
|
||||
/// Takes a list of tokens and a choreo record, which is searched
|
||||
/// through for each token for its construct use type.
|
||||
/// Also takes either an event (typically when the Representation itself is
|
||||
/// available) or construct use metadata (when the event is not available,
|
||||
/// i.e. immediately after message send) to create the construct use.
|
||||
List<OneConstructUse> vocabUses({
|
||||
List<OneConstructUse> vocabAndMorphUses({
|
||||
required List<PangeaToken> tokens,
|
||||
Event? event,
|
||||
ConstructUseMetaData? metadata,
|
||||
|
|
@ -120,7 +120,7 @@ class PangeaRepresentation {
|
|||
tokens.where((token) => token.lemma.saveVocab).toList();
|
||||
for (final token in tokensToSave) {
|
||||
uses.addAll(
|
||||
getUsesForToken(
|
||||
_getUsesForToken(
|
||||
token,
|
||||
metadata,
|
||||
choreo: choreo,
|
||||
|
|
@ -135,10 +135,12 @@ class PangeaRepresentation {
|
|||
/// If there is no [choreo], the [token] is
|
||||
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
|
||||
/// Later on, we may want to consider putting it in some category of like 'pending'
|
||||
/// If the [token] is in the [choreo.acceptedOrIgnoredMatch], it is considered to be a [ConstructUseTypeEnum.ga].
|
||||
/// If the [token] is in the [choreo.acceptedOrIgnoredMatch.choices], it is considered to be a [ConstructUseTypeEnum.corIt].
|
||||
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
|
||||
List<OneConstructUse> getUsesForToken(
|
||||
///
|
||||
/// For both vocab and morph constructs, we should
|
||||
/// 1) give wa if no assistance was used
|
||||
/// 2) give ga if IGC was used and
|
||||
/// 3) make no use if IT was used
|
||||
List<OneConstructUse> _getUsesForToken(
|
||||
PangeaToken token,
|
||||
ConstructUseMetaData metadata, {
|
||||
ChoreoRecord? choreo,
|
||||
|
|
@ -167,7 +169,7 @@ class PangeaRepresentation {
|
|||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
lemma.toVocabUse(
|
||||
inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk,
|
||||
useType,
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
|
|
@ -180,8 +182,12 @@ class PangeaRepresentation {
|
|||
/// is in the overall step text, then token was a ga
|
||||
final bool isAcceptedMatch =
|
||||
step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted;
|
||||
final bool isITStep = step.itStep != null;
|
||||
if (!isAcceptedMatch && !isITStep) continue;
|
||||
|
||||
// if the token was in an IT match, return no uses
|
||||
if (step.itStep != null) return [];
|
||||
|
||||
// if this step was not accepted, continue
|
||||
if (!isAcceptedMatch) continue;
|
||||
|
||||
if (isAcceptedMatch &&
|
||||
step.acceptedOrIgnoredMatch?.match.choices != null) {
|
||||
|
|
@ -189,25 +195,33 @@ class PangeaRepresentation {
|
|||
final bool stepContainedToken = choices.any(
|
||||
(choice) =>
|
||||
// if this choice contains the token's content
|
||||
choice.value.contains(content) &&
|
||||
// if the complete input text after this step
|
||||
// contains the choice (why is this here?)
|
||||
step.text.contains(choice.value),
|
||||
choice.value.contains(content),
|
||||
);
|
||||
if (stepContainedToken) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (isITStep && step.itStep?.chosenContinuance != null) {
|
||||
final bool pickedThroughIT =
|
||||
step.itStep!.chosenContinuance!.text.contains(content);
|
||||
if (pickedThroughIT) {
|
||||
return [];
|
||||
// give ga if IGC was used
|
||||
uses.add(
|
||||
lemma.toVocabUse(
|
||||
ConstructUseTypeEnum.ga,
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
for (final entry in token.morph.entries) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
lemma: entry.value,
|
||||
categories: [entry.key],
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the token wasn't found in any IT or IGC step, so it was wa
|
||||
for (final entry in token.morph.entries) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
|
|
|
|||
|
|
@ -1,572 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ConstructList extends StatefulWidget {
|
||||
final ConstructTypeEnum constructType;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? selected;
|
||||
final TimeSpan timeSpan;
|
||||
final PangeaController pangeaController;
|
||||
final StreamController refreshStream;
|
||||
|
||||
const ConstructList({
|
||||
super.key,
|
||||
required this.constructType,
|
||||
required this.defaultSelected,
|
||||
required this.pangeaController,
|
||||
required this.refreshStream,
|
||||
required this.timeSpan,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => ConstructListState();
|
||||
}
|
||||
|
||||
class ConstructListState extends State<ConstructList> {
|
||||
String? langCode;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return error != null
|
||||
? Center(
|
||||
child: Text(error!),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
ConstructListView(
|
||||
pangeaController: widget.pangeaController,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
refreshStream: widget.refreshStream,
|
||||
timeSpan: widget.timeSpan,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// list view of construct events
|
||||
// parameters
|
||||
// 1) a list of construct events and
|
||||
// 2) a boolean indicating whether the list has been initialized
|
||||
// if not initialized, show loading indicator
|
||||
// for each tile,
|
||||
// title = construct.content.lemma
|
||||
// subtitle = total uses, equal to construct.content.uses.length
|
||||
// list has a fixed height of 400 and is scrollable
|
||||
class ConstructListView extends StatefulWidget {
|
||||
final PangeaController pangeaController;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final TimeSpan timeSpan;
|
||||
final AnalyticsSelected? selected;
|
||||
final StreamController refreshStream;
|
||||
|
||||
const ConstructListView({
|
||||
super.key,
|
||||
required this.pangeaController,
|
||||
required this.defaultSelected,
|
||||
required this.timeSpan,
|
||||
required this.refreshStream,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => ConstructListViewState();
|
||||
}
|
||||
|
||||
class ConstructListViewState extends State<ConstructListView> {
|
||||
final ConstructTypeEnum constructType = ConstructTypeEnum.grammar;
|
||||
final Map<String, Timeline> _timelinesCache = {};
|
||||
final Map<String, PangeaMessageEvent> _msgEventCache = {};
|
||||
final List<PangeaMessageEvent> _msgEvents = [];
|
||||
bool fetchingConstructs = true;
|
||||
bool fetchingUses = false;
|
||||
StreamSubscription? refreshSubscription;
|
||||
String? currentLemma;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.whenComplete(() => setState(() => fetchingConstructs = false))
|
||||
.then(
|
||||
(value) => setState(
|
||||
() => constructs = ConstructListModel(
|
||||
type: constructType,
|
||||
uses: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
|
||||
// postframe callback to let widget rebuild with the new selected parameter
|
||||
// before sending selected to getConstructs function
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.then(
|
||||
(value) => setState(() {
|
||||
ConstructListModel(
|
||||
type: constructType,
|
||||
uses: value,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<PangeaMessageEvent?> getMessageEvent(
|
||||
OneConstructUse use,
|
||||
) async {
|
||||
final Client client = Matrix.of(context).client;
|
||||
PangeaMessageEvent msgEvent;
|
||||
if (_msgEventCache.containsKey(use.msgId)) {
|
||||
return _msgEventCache[use.msgId]!;
|
||||
}
|
||||
final Room? msgRoom = use.getRoom(client);
|
||||
if (msgRoom == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Timeline? timeline;
|
||||
if (_timelinesCache.containsKey(use.chatId)) {
|
||||
timeline = _timelinesCache[use.chatId];
|
||||
} else {
|
||||
timeline = msgRoom.timeline ?? await msgRoom.getTimeline();
|
||||
_timelinesCache[use.chatId] = timeline;
|
||||
}
|
||||
|
||||
final Event? event = await use.getEvent(client);
|
||||
if (event == null || timeline == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
msgEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: event.senderId == client.userID,
|
||||
);
|
||||
_msgEventCache[use.msgId] = msgEvent;
|
||||
return msgEvent;
|
||||
}
|
||||
|
||||
Future<void> fetchUses() async {
|
||||
if (fetchingUses) return;
|
||||
if (currentLemma == null) {
|
||||
setState(() => _msgEvents.clear());
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => fetchingUses = true);
|
||||
try {
|
||||
final List<OneConstructUse> uses = constructs?.constructList
|
||||
.firstWhereOrNull(
|
||||
(element) => element.lemma == currentLemma,
|
||||
)
|
||||
?.uses ??
|
||||
[];
|
||||
_msgEvents.clear();
|
||||
|
||||
for (final OneConstructUse use in uses) {
|
||||
final PangeaMessageEvent? msgEvent = await getMessageEvent(use);
|
||||
final RepresentationEvent? repEvent =
|
||||
msgEvent?.originalSent ?? msgEvent?.originalWritten;
|
||||
if (repEvent?.choreo == null) {
|
||||
continue;
|
||||
}
|
||||
_msgEvents.add(msgEvent!);
|
||||
}
|
||||
setState(() => fetchingUses = false);
|
||||
} catch (err, s) {
|
||||
setState(() => fetchingUses = false);
|
||||
debugPrint("Error fetching uses: $err");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
m: "Failed to fetch uses for current construct $currentLemma",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConstructListModel? constructs;
|
||||
|
||||
// given the current lemma and list of message events, return a list of
|
||||
// MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch
|
||||
// this is because some message events may have has more than one PangeaMatch of a
|
||||
// given lemma type.
|
||||
List<MessageEventMatch> getMessageEventMatches() {
|
||||
if (currentLemma == null) return [];
|
||||
final List<MessageEventMatch> allMsgErrorSteps = [];
|
||||
|
||||
for (final msgEvent in _msgEvents) {
|
||||
if (allMsgErrorSteps.any(
|
||||
(element) => element.msgEvent.eventId == msgEvent.eventId,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
// get all the pangea matches in that message which have that lemma
|
||||
final List<PangeaMatch>? msgErrorSteps = msgEvent.errorSteps(
|
||||
currentLemma!,
|
||||
);
|
||||
if (msgErrorSteps == null) continue;
|
||||
|
||||
allMsgErrorSteps.addAll(
|
||||
msgErrorSteps.map(
|
||||
(errorStep) => MessageEventMatch(
|
||||
msgEvent: msgEvent,
|
||||
lemmaMatch: errorStep,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return allMsgErrorSteps;
|
||||
}
|
||||
|
||||
Future<void> showConstructMessagesDialog() async {
|
||||
await showDialog<ConstructMessagesDialog>(
|
||||
context: context,
|
||||
builder: (c) => ConstructMessagesDialog(controller: this),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (fetchingConstructs || fetchingUses) {
|
||||
return const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (constructs?.constructList.isEmpty ?? true) {
|
||||
return Expanded(
|
||||
child: Center(child: Text(L10n.of(context)!.noDataFound)),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.constructList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs!.constructList[index].lemma,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs!.constructList[index].uses.length}',
|
||||
),
|
||||
onTap: () async {
|
||||
final String lemma = constructs!.constructList[index].lemma;
|
||||
setCurrentLemma(lemma);
|
||||
fetchUses().then((_) => showConstructMessagesDialog());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessagesDialog extends StatelessWidget {
|
||||
final ConstructListViewState controller;
|
||||
const ConstructMessagesDialog({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.currentLemma == null || controller.constructs == null) {
|
||||
return const AlertDialog(content: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final msgEventMatches = controller.getMessageEventMatches();
|
||||
|
||||
final currentConstruct =
|
||||
controller.constructs!.constructList.firstWhereOrNull(
|
||||
(construct) => construct.lemma == controller.currentLemma,
|
||||
);
|
||||
final noData = currentConstruct == null ||
|
||||
currentConstruct.uses.length > controller._msgEvents.length;
|
||||
|
||||
return AlertDialog(
|
||||
title: Center(child: Text(controller.currentLemma!)),
|
||||
content: SizedBox(
|
||||
height: noData ? 90 : 250,
|
||||
width: noData ? 200 : 400,
|
||||
child: Column(
|
||||
children: [
|
||||
if (noData)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context)!.roomDataMissing),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
...msgEventMatches.mapIndexed(
|
||||
(index, event) => Column(
|
||||
children: [
|
||||
ConstructMessage(
|
||||
msgEvent: event.msgEvent,
|
||||
lemma: controller.currentLemma!,
|
||||
errorMessage: event.lemmaMatch,
|
||||
),
|
||||
if (index < msgEventMatches.length - 1)
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
|
||||
child: Text(
|
||||
L10n.of(context)!.close.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessage extends StatelessWidget {
|
||||
final PangeaMessageEvent msgEvent;
|
||||
final PangeaMatch errorMessage;
|
||||
final String lemma;
|
||||
|
||||
const ConstructMessage({
|
||||
super.key,
|
||||
required this.msgEvent,
|
||||
required this.errorMessage,
|
||||
required this.lemma,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String? chosen = errorMessage.match.choices
|
||||
?.firstWhereOrNull(
|
||||
(element) => element.selected == true,
|
||||
)
|
||||
?.value;
|
||||
|
||||
if (chosen == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
ConstructMessageMetadata(msgEvent: msgEvent),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FutureBuilder<User?>(
|
||||
future: msgEvent.event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
final displayname = snapshot.data?.calcDisplayname() ??
|
||||
msgEvent.event.senderFromMemoryOrFallback
|
||||
.calcDisplayname();
|
||||
return Text(
|
||||
displayname,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? displayname.color
|
||||
: displayname.lightColorText),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ConstructMessageBubble(
|
||||
errorText: errorMessage.match.fullText,
|
||||
replacementText: chosen,
|
||||
start: errorMessage.match.offset,
|
||||
end:
|
||||
errorMessage.match.offset + errorMessage.match.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessageBubble extends StatelessWidget {
|
||||
final String errorText;
|
||||
final String replacementText;
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
const ConstructMessageBubble({
|
||||
super.key,
|
||||
required this.errorText,
|
||||
required this.replacementText,
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyle = TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
return IntrinsicWidth(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
bottomLeft: Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: errorText.substring(0, start),
|
||||
style: defaultStyle,
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(start, end),
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.red.withOpacity(0.25),
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationThickness: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text: replacementText,
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.green.withOpacity(0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(end),
|
||||
style: defaultStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessageMetadata extends StatelessWidget {
|
||||
final PangeaMessageEvent msgEvent;
|
||||
|
||||
const ConstructMessageMetadata({
|
||||
super.key,
|
||||
required this.msgEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String roomName = msgEvent.event.room.getLocalizedDisplayname();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 30, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
msgEvent.event.originServerTs.localizedTime(context),
|
||||
style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor),
|
||||
),
|
||||
Text(roomName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageEventMatch {
|
||||
final PangeaMessageEvent msgEvent;
|
||||
final PangeaMatch lemmaMatch;
|
||||
|
||||
MessageEventMatch({
|
||||
required this.msgEvent,
|
||||
required this.lemmaMatch,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
// import 'dart:math';
|
||||
|
||||
// import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
// import '../../enum/use_type.dart';
|
||||
|
||||
// class ListSummaryAnalytics extends StatelessWidget {
|
||||
// final ChartAnalyticsModel? chartAnalytics;
|
||||
|
||||
// const ListSummaryAnalytics({super.key, this.chartAnalytics});
|
||||
|
||||
// TimeSeriesTotals? get totals => chartAnalytics?.totals;
|
||||
|
||||
// String spacer(int baseLength, int number) =>
|
||||
// " " * max(baseLength - number.toString().length, 0);
|
||||
|
||||
// WidgetSpan spacerIconText(
|
||||
// String toolTip,
|
||||
// String space,
|
||||
// IconData icon,
|
||||
// int value,
|
||||
// Color? color, [
|
||||
// percentage = true,
|
||||
// ]) =>
|
||||
// WidgetSpan(
|
||||
// child: Tooltip(
|
||||
// message: toolTip,
|
||||
// child: RichText(
|
||||
// text: TextSpan(
|
||||
// children: [
|
||||
// TextSpan(
|
||||
// text: space,
|
||||
// ),
|
||||
// WidgetSpan(child: Icon(icon, size: 14, color: color)),
|
||||
// TextSpan(
|
||||
// text: " $value${percentage ? "%" : ""}",
|
||||
// style: TextStyle(color: color),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (totals == null) {
|
||||
// return const LinearProgressIndicator();
|
||||
// }
|
||||
// final l10n = L10n.of(context);
|
||||
|
||||
// return RichText(
|
||||
// text: TextSpan(
|
||||
// children: [
|
||||
// spacerIconText(
|
||||
// L10n.of(context) != null
|
||||
// ? L10n.of(context)!.totalMessages
|
||||
// : "Total messages sent",
|
||||
// "",
|
||||
// Icons.chat_bubble,
|
||||
// totals!.all,
|
||||
// Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// false,
|
||||
// ),
|
||||
// if (totals!.all != 0) ...[
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.taTooltip : "With translation assistance",
|
||||
// spacer(8, totals!.all),
|
||||
// UseType.ta.iconData,
|
||||
// totals!.taPercent,
|
||||
// UseType.ta.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.gaTooltip : "With grammar assistance",
|
||||
// spacer(4, totals!.taPercent),
|
||||
// UseType.ga.iconData,
|
||||
// totals!.gaPercent,
|
||||
// UseType.ga.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.waTooltip : "Without assistance",
|
||||
// spacer(4, totals!.gaPercent),
|
||||
// UseType.wa.iconData,
|
||||
// totals!.waPercent,
|
||||
// UseType.wa.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.unTooltip : "Other",
|
||||
// spacer(4, totals!.waPercent),
|
||||
// UseType.un.iconData,
|
||||
// totals!.unPercent,
|
||||
// UseType.un.color(context),
|
||||
// ),
|
||||
// ],
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
@ -20,7 +20,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
|
|||
final matrix = Matrix.of(context);
|
||||
|
||||
// before wiping out locally cached construct data, save it to the server
|
||||
await MatrixState.pangeaController.myAnalytics
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.sendLocalAnalyticsToAnalyticsRoom(onLogout: true);
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -29,8 +29,8 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
late Animation<double> _fadeAnimation;
|
||||
|
||||
StreamSubscription? _pointsSubscription;
|
||||
int? get _prevXP => MatrixState.pangeaController.analytics.prevXP;
|
||||
int? get _currentXP => MatrixState.pangeaController.analytics.currentXP;
|
||||
int? get _prevXP => MatrixState.pangeaController.getAnalytics.prevXP;
|
||||
int? get _currentXP => MatrixState.pangeaController.getAnalytics.currentXP;
|
||||
int? _addedPoints;
|
||||
|
||||
@override
|
||||
|
|
@ -62,7 +62,7 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
|
|||
);
|
||||
|
||||
_pointsSubscription = MatrixState
|
||||
.pangeaController.analytics.analyticsStream.stream
|
||||
.pangeaController.getAnalytics.analyticsStream.stream
|
||||
.listen(_showPointsGained);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,21 +54,21 @@ class LearningProgressIndicatorsState
|
|||
// int get totalXP => _pangeaController.analytics.currentXP;
|
||||
// int get level => _pangeaController.analytics.level;
|
||||
List<OneConstructUse> currentConstructs = [];
|
||||
int get currentXP => _pangeaController.analytics.calcXP(currentConstructs);
|
||||
int get localXP => _pangeaController.analytics.calcXP(
|
||||
_pangeaController.analytics.locallyCachedConstructs,
|
||||
int get currentXP => _pangeaController.getAnalytics.calcXP(currentConstructs);
|
||||
int get localXP => _pangeaController.getAnalytics.calcXP(
|
||||
_pangeaController.getAnalytics.locallyCachedConstructs,
|
||||
);
|
||||
int get serverXP => currentXP - localXP;
|
||||
int get level => _pangeaController.analytics.level;
|
||||
int get level => _pangeaController.getAnalytics.level;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
updateAnalyticsData(
|
||||
_pangeaController.analytics.analyticsStream.value?.constructs ?? [],
|
||||
_pangeaController.getAnalytics.analyticsStream.value?.constructs ?? [],
|
||||
);
|
||||
_analyticsUpdateSubscription = _pangeaController
|
||||
.analytics.analyticsStream.stream
|
||||
.getAnalytics.analyticsStream.stream
|
||||
.listen((update) => updateAnalyticsData(update.constructs));
|
||||
}
|
||||
|
||||
|
|
@ -146,12 +146,12 @@ class LearningProgressIndicatorsState
|
|||
? const Color.fromARGB(255, 0, 190, 83)
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: currentXP,
|
||||
widthMultiplier: _pangeaController.analytics.levelProgress,
|
||||
widthMultiplier: _pangeaController.getAnalytics.levelProgress,
|
||||
),
|
||||
LevelBarDetails(
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: serverXP,
|
||||
widthMultiplier: _pangeaController.analytics.serverLevelProgress,
|
||||
widthMultiplier: _pangeaController.getAnalytics.serverLevelProgress,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
|
@ -125,7 +125,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
selectedChoiceIndex = index;
|
||||
if (selectedChoice != null) {
|
||||
if (!selectedChoice!.selected) {
|
||||
MatrixState.pangeaController.myAnalytics.addDraftUses(
|
||||
MatrixState.pangeaController.putAnalytics.addDraftUses(
|
||||
selectedChoice!.tokens,
|
||||
widget.roomId,
|
||||
selectedChoice!.isBestCorrection
|
||||
|
|
@ -158,7 +158,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
|
||||
/// Adds the ignored tokens to locally cached analytics
|
||||
void addIgnoredTokenUses() {
|
||||
MatrixState.pangeaController.myAnalytics.addDraftUses(
|
||||
MatrixState.pangeaController.putAnalytics.addDraftUses(
|
||||
ignoredTokens ?? [],
|
||||
widget.roomId,
|
||||
ConstructUseTypeEnum.ignIGC,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
|
|
@ -73,7 +73,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
MatrixState.pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class TargetTokensController {
|
|||
_targetTokens = await _initialize(pangeaMessageEvent);
|
||||
|
||||
final allConstructs = MatrixState
|
||||
.pangeaController.analytics.analyticsStream.value?.constructs;
|
||||
.pangeaController.getAnalytics.analyticsStream.value?.constructs;
|
||||
await updateTokensWithConstructs(
|
||||
allConstructs ?? [],
|
||||
pangeaMessageEvent,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
|
|
@ -69,7 +69,7 @@ class WordFocusListeningActivityState
|
|||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
MatrixState.pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId:
|
||||
|
|
|
|||
|
|
@ -93,30 +93,18 @@ Future<void> pLanguageDialog(
|
|||
context: context,
|
||||
future: () async {
|
||||
try {
|
||||
pangeaController.myAnalytics
|
||||
.sendLocalAnalyticsToAnalyticsRoom()
|
||||
.then((_) {
|
||||
pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.sourceLanguage =
|
||||
selectedSourceLanguage.langCode;
|
||||
profile.userSettings.targetLanguage =
|
||||
selectedTargetLanguage.langCode;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
}).then((_) {
|
||||
// if the profile update is successful, reset cached analytics
|
||||
// data, since analytics data corresponds to the user's L2
|
||||
pangeaController.myAnalytics.dispose();
|
||||
pangeaController.analytics.dispose();
|
||||
|
||||
pangeaController.myAnalytics.initialize();
|
||||
pangeaController.analytics.initialize();
|
||||
|
||||
Navigator.pop(context);
|
||||
});
|
||||
await pangeaController.userController
|
||||
.updateProfile(
|
||||
(profile) {
|
||||
profile.userSettings.sourceLanguage =
|
||||
selectedSourceLanguage.langCode;
|
||||
profile.userSettings.targetLanguage =
|
||||
selectedTargetLanguage.langCode;
|
||||
return profile;
|
||||
},
|
||||
waitForDataInSync: true,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue