diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 7d1370295..5d6286404 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -845,6 +845,7 @@ class ChatListController extends State if (mounted) { GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); + await pangeaController.myAnalytics.addEventsListener(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); diff --git a/lib/pangea/constants/local.key.dart b/lib/pangea/constants/local.key.dart index c0390c2ba..743fe1143 100644 --- a/lib/pangea/constants/local.key.dart +++ b/lib/pangea/constants/local.key.dart @@ -11,4 +11,5 @@ class PLocalKey { static const String dismissedPaywall = 'dismissedPaywall'; static const String paywallBackoff = 'paywallBackoff'; static const String autoPlayMessages = 'autoPlayMessages'; + static const String messagesSinceUpdate = 'messagesSinceLastUpdate'; } diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 2ea2d215a..43b119087 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer'; import 'package:collection/collection.dart'; @@ -5,20 +6,24 @@ import 'package:fluffychat/pangea/constants/match_rule_ids.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/models/constructs_event.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_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 '../models/chart_analytics_model.dart'; +import '../models/analytics/chart_analytics_model.dart'; import 'base_controller.dart'; import 'pangea_controller.dart'; +// controls the fetching of analytics data class AnalyticsController extends BaseController { late PangeaController _pangeaController; @@ -29,6 +34,7 @@ class AnalyticsController extends BaseController { _pangeaController = pangeaController; } + ///////// TIME SPANS ////////// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY"; TimeSpan get currentAnalyticsTimeSpan { @@ -57,50 +63,137 @@ class AnalyticsController extends BaseController { ); } - Future> allMySummaryAnalytics() async { + Future myAnalyticsLastUpdated(String type) async { + // given an analytics event type, get the last updated times + // for each of the user's analytics rooms and return the most recent + // Most Recent instead of the oldest because, for instance: + // My last Spanish event was sent 3 days ago. + // My last English event was sent 1 day ago. + // When I go to check if the cached data is out of date, the cached item was set 2 days ago. + // I know there’s new data available because the English update data (the most recent) is after the cache’s creation time. + // So, I should update the cache. + final List analyticsRooms = _pangeaController + .matrixState.client.allMyAnalyticsRooms + .where((room) => room.isAnalyticsRoom) + .toList(); + + final List lastUpdates = []; + for (final Room analyticsRoom in analyticsRooms) { + final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( + type, + _pangeaController.matrixState.client.userID!, + ); + if (lastUpdated != null) { + lastUpdates.add(lastUpdated); + } + } + + if (lastUpdates.isEmpty) return null; + return lastUpdates.reduce( + (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, + ); + } + + Future spaceAnalyticsLastUpdated( + String type, + Room space, + String langCode, + ) async { + // check if any students have recently updated their analytics + // if any have, then the cache needs to be updated + // TODO - figure out how to do this on a per-student basis + await space.requestParticipants(); + + final List> 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( + type, + student.id, + ), + ); + } + + final List lastUpdatedWithNulls = + await Future.wait(lastUpdatedFutures); + final List lastUpdates = + lastUpdatedWithNulls.where((e) => e != null).cast().toList(); + if (lastUpdates.isNotEmpty) { + return lastUpdates.reduce( + (check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent, + ); + } + return null; + } + + //////////////////////////// MESSAGE SUMMARY ANALYTICS //////////////////////////// + + Future> mySummaryAnalytics() async { + // gets all the summary analytics events for the user + // since the current timespace's cut off date final analyticsRooms = _pangeaController.matrixState.client.allMyAnalyticsRooms; final List allEvents = []; + + // TODO switch to using list of futures for (final Room analyticsRoom in analyticsRooms) { - final List? roomEvents = - (await analyticsRoom.getAnalyticsEvents( + final List? roomEvents = + await analyticsRoom.getAnalyticsEvents( type: PangeaEventTypes.summaryAnalytics, since: currentAnalyticsTimeSpan.cutOffDate, - )) - ?.cast(); - allEvents.addAll(roomEvents ?? []); + userId: _pangeaController.matrixState.client.userID!, + ); + + allEvents.addAll( + roomEvents?.cast() ?? [], + ); } return allEvents; } - Future> allSpaceMemberAnalytics( + Future> spaceMemberAnalytics( Room space, ) async { + // gets all the summary analytics events for the students + // in a space since the current timespace's cut off date final langCode = _pangeaController.languageController.activeL2Code( roomID: space.id, ); - final List analyticsEvents = []; + + // ensure that all the space's events are loaded (mainly the for langCode) + // and that the participants are loaded await space.postLoad(); await space.requestParticipants(); + + // TODO switch to using list of futures + final List analyticsEvents = []; for (final student in space.students) { final Room? analyticsRoom = _pangeaController.matrixState.client .analyticsRoomLocal(langCode, student.id); + if (analyticsRoom != null) { - final List? roomEvents = - (await analyticsRoom.getAnalyticsEvents( + final List? roomEvents = + await analyticsRoom.getAnalyticsEvents( type: PangeaEventTypes.summaryAnalytics, since: currentAnalyticsTimeSpan.cutOffDate, - )) - ?.cast(); - analyticsEvents.addAll(roomEvents ?? []); + userId: student.id, + ); + analyticsEvents.addAll( + roomEvents?.cast() ?? [], + ); } } + // get a list of all the space's children, including sub-space children final resp = await space.client.getSpaceHierarchy(space.id); final List spaceChildrenIds = resp.rooms.map((room) => room.roomId).toList(); + // filter out the analyics events that don't belong to the space's children final List allAnalyticsEvents = []; for (final analyticsEvent in analyticsEvents) { analyticsEvent.content.messages.removeWhere( @@ -115,10 +208,10 @@ class AnalyticsController extends BaseController { ChartAnalyticsModel? getAnalyticsLocal({ TimeSpan? timeSpan, required AnalyticsSelected defaultSelected, - required DateTime? analyticsLastUpdated, AnalyticsSelected? selected, bool forceUpdate = false, bool updateExpired = false, + DateTime? lastUpdated, }) { timeSpan ??= currentAnalyticsTimeSpan; final int index = _cachedAnalyticsModels.indexWhere( @@ -131,11 +224,9 @@ class AnalyticsController extends BaseController { ); if (index != -1) { - final DateTime? cachedLastUpdate = - _cachedAnalyticsModels[index].summaryLastUpdated; if ((updateExpired && _cachedAnalyticsModels[index].isExpired) || forceUpdate || - cachedLastUpdate != analyticsLastUpdated) { + _cachedAnalyticsModels[index].needsUpdate(lastUpdated)) { _cachedAnalyticsModels.removeAt(index); } else { return _cachedAnalyticsModels[index].chartAnalyticsModel; @@ -148,7 +239,6 @@ class AnalyticsController extends BaseController { void cacheAnalytics({ required ChartAnalyticsModel chartAnalyticsModel, required AnalyticsSelected defaultSelected, - required DateTime? summaryLastUpdated, AnalyticsSelected? selected, TimeSpan? timeSpan, }) { @@ -158,7 +248,6 @@ class AnalyticsController extends BaseController { chartAnalyticsModel: chartAnalyticsModel, defaultSelected: defaultSelected, selected: selected, - summaryLastUpdated: summaryLastUpdated, ), ); } @@ -256,6 +345,14 @@ class AnalyticsController extends BaseController { Room? space, AnalyticsSelected? selected, }) async { + for (int i = 0; i < unfilteredAnalytics.length; i++) { + unfilteredAnalytics[i].content.messages.removeWhere( + (record) => record.time.isBefore( + currentAnalyticsTimeSpan.cutOffDate, + ), + ); + } + switch (selected?.type) { case null: return unfilteredAnalytics; @@ -286,20 +383,11 @@ class AnalyticsController extends BaseController { bool forceUpdate = false, }) async { try { - final DateTime? analyticsLastUpdated = await _pangeaController.myAnalytics - .analyticsLastUpdated(PangeaEventTypes.summaryAnalytics); - final local = getAnalyticsLocal( - defaultSelected: defaultSelected, - selected: selected, - forceUpdate: forceUpdate, - analyticsLastUpdated: analyticsLastUpdated, - ); - if (local != null && !forceUpdate) { - return local; - } - await _pangeaController.matrixState.client.roomsLoading; + + // if the user is looking at space analytics, then fetch the space Room? space; + String? langCode; if (defaultSelected.type == AnalyticsEntryType.space) { space = _pangeaController.matrixState.client.getRoomById( defaultSelected.id, @@ -317,13 +405,60 @@ class AnalyticsController extends BaseController { timeSpan: currentAnalyticsTimeSpan, ); } + + await space.postLoad(); + langCode = _pangeaController.languageController.activeL2Code( + roomID: space.id, + ); + if (langCode == null) { + ErrorHandler.logError( + m: "langCode missing in getAnalytics", + data: { + "space": space, + }, + ); + return ChartAnalyticsModel( + msgs: [], + timeSpan: currentAnalyticsTimeSpan, + ); + } } + 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( + PangeaEventTypes.summaryAnalytics, + ); + } else { + // else, get the last time a student in the space updated their analytics + lastUpdated = await spaceAnalyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + space!, + langCode!, + ); + } + + final ChartAnalyticsModel? local = getAnalyticsLocal( + defaultSelected: defaultSelected, + selected: selected, + forceUpdate: forceUpdate, + lastUpdated: lastUpdated, + ); + if (local != null && !forceUpdate) { + return local; + } + + // get all the relevant summary analytics events for the current timespan final List summaryEvents = defaultSelected.type == AnalyticsEntryType.space - ? await allSpaceMemberAnalytics(space!) - : await allMySummaryAnalytics(); + ? await spaceMemberAnalytics(space!) + : await mySummaryAnalytics(); + // filter out the analytics events based on filters the user has chosen final List filteredAnalytics = await filterAnalytics( unfilteredAnalytics: summaryEvents, @@ -332,6 +467,7 @@ class AnalyticsController extends BaseController { selected: selected, ); + // then create and return the model to be displayed final ChartAnalyticsModel newModel = ChartAnalyticsModel( timeSpan: currentAnalyticsTimeSpan, msgs: filteredAnalytics @@ -340,15 +476,12 @@ class AnalyticsController extends BaseController { .toList(), ); - if (local == null) { - cacheAnalytics( - chartAnalyticsModel: newModel, - defaultSelected: defaultSelected, - selected: selected, - timeSpan: currentAnalyticsTimeSpan, - summaryLastUpdated: analyticsLastUpdated, - ); - } + cacheAnalytics( + chartAnalyticsModel: newModel, + defaultSelected: defaultSelected, + selected: selected, + timeSpan: currentAnalyticsTimeSpan, + ); return newModel; } catch (err, s) { @@ -379,6 +512,8 @@ class AnalyticsController extends BaseController { final List? roomEvents = (await analyticsRoom.getAnalyticsEvents( type: PangeaEventTypes.construct, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: _pangeaController.matrixState.client.userID!, )) ?.cast(); allConstructs.addAll(roomEvents ?? []); @@ -430,6 +565,8 @@ class AnalyticsController extends BaseController { final List? roomEvents = (await analyticsRoom.getAnalyticsEvents( type: PangeaEventTypes.construct, + since: currentAnalyticsTimeSpan.cutOffDate, + userId: student.id, )) ?.cast(); constructEvents.addAll(roomEvents ?? []); @@ -525,8 +662,8 @@ class AnalyticsController extends BaseController { required TimeSpan timeSpan, required ConstructType constructType, required AnalyticsSelected defaultSelected, - required DateTime? constructsLastUpdated, AnalyticsSelected? selected, + DateTime? lastUpdated, }) { final index = _cachedConstructs.indexWhere( (e) => @@ -539,8 +676,7 @@ class AnalyticsController extends BaseController { ); if (index > -1) { - if (_cachedConstructs[index].constructsLastUpdated != - constructsLastUpdated) { + if (_cachedConstructs[index].needsUpdate(lastUpdated)) { _cachedConstructs.removeAt(index); return null; } @@ -554,19 +690,16 @@ class AnalyticsController extends BaseController { required ConstructType constructType, required List events, required AnalyticsSelected defaultSelected, - required DateTime? constructsLastUpdated, AnalyticsSelected? selected, }) { - _cachedConstructs.add( - ConstructCacheEntry( - timeSpan: currentAnalyticsTimeSpan, - type: constructType, - events: events, - defaultSelected: defaultSelected, - selected: selected, - constructsLastUpdated: constructsLastUpdated, - ), + final entry = ConstructCacheEntry( + timeSpan: currentAnalyticsTimeSpan, + type: constructType, + events: List.from(events), + defaultSelected: defaultSelected, + selected: selected, ); + _cachedConstructs.add(entry); } Future> getMyConstructs({ @@ -661,31 +794,75 @@ class AnalyticsController extends BaseController { required ConstructType constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, - bool removeIT = false, + bool removeIT = true, bool forceUpdate = false, }) async { - final DateTime? constructsLastUpdated = await _pangeaController.myAnalytics - .analyticsLastUpdated(PangeaEventTypes.construct); + if (settingConstructs) return _constructs; + settingConstructs = true; + await _pangeaController.matrixState.client.roomsLoading; + + Room? space; + String? langCode; + 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, + }, + ); + settingConstructs = false; + return _constructs; + } + await space.postLoad(); + langCode = _pangeaController.languageController.activeL2Code( + roomID: space.id, + ); + if (langCode == null) { + ErrorHandler.logError( + m: "langCode missing in setConstructs", + data: { + "space": space, + }, + ); + settingConstructs = false; + return _constructs; + } + } + + 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( + PangeaEventTypes.construct, + ); + } else { + // else, get the last time a student in the space updated their analytics + lastUpdated = await spaceAnalyticsLastUpdated( + PangeaEventTypes.construct, + space!, + langCode!, + ); + } + final List? local = getConstructsLocal( timeSpan: currentAnalyticsTimeSpan, constructType: constructType, defaultSelected: defaultSelected, selected: selected, - constructsLastUpdated: constructsLastUpdated, + lastUpdated: lastUpdated, ); if (local != null && !forceUpdate) { _constructs = local; - return _constructs; - } - - if (settingConstructs) return _constructs; - settingConstructs = true; - await _pangeaController.matrixState.client.roomsLoading; - Room? space; - if (defaultSelected.type == AnalyticsEntryType.space) { - space = _pangeaController.matrixState.client.getRoomById( - defaultSelected.id, - ); + settingConstructs = false; + return local; } final filteredConstructs = space == null @@ -720,46 +897,48 @@ class AnalyticsController extends BaseController { events: _constructs!, defaultSelected: defaultSelected, selected: selected, - constructsLastUpdated: constructsLastUpdated, ); } settingConstructs = false; return _constructs; } + + // used to aggregate ConstructEvents from + // multiple senders (students) with the same lemma + List aggregateConstructData( + List constructs, + ) { + final Map> lemmasToConstructs = {}; + for (final construct in constructs) { + for (final lemmaUses in construct.content.uses) { + lemmasToConstructs[lemmaUses.lemma] ??= []; + lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses); + } + } + + final List aggregatedConstructs = []; + for (final lemmaToConstructs in lemmasToConstructs.entries) { + final List lemmaConstructs = + lemmaToConstructs.value; + final AggregateConstructUses aggregatedData = AggregateConstructUses( + lemmaUses: lemmaConstructs, + ); + aggregatedConstructs.add(aggregatedData); + } + return aggregatedConstructs; + } } -class ConstructCacheEntry { +abstract class CacheEntry { final TimeSpan timeSpan; - final ConstructType type; - final List events; final AnalyticsSelected defaultSelected; AnalyticsSelected? selected; - final DateTime? constructsLastUpdated; + late final DateTime _createdAt; - ConstructCacheEntry({ + CacheEntry({ required this.timeSpan, - required this.type, - required this.events, required this.defaultSelected, - required this.constructsLastUpdated, - this.selected, - }); -} - -class AnalyticsCacheModel { - TimeSpan timeSpan; - ChartAnalyticsModel chartAnalyticsModel; - final AnalyticsSelected defaultSelected; - AnalyticsSelected? selected; - late DateTime _createdAt; - final DateTime? summaryLastUpdated; - - AnalyticsCacheModel({ - required this.timeSpan, - required this.chartAnalyticsModel, - required this.defaultSelected, - required this.summaryLastUpdated, this.selected, }) { _createdAt = DateTime.now(); @@ -768,4 +947,47 @@ class AnalyticsCacheModel { 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 ConstructType type; + final List events; + + ConstructCacheEntry({ + required this.type, + required this.events, + required super.timeSpan, + required super.defaultSelected, + super.selected, + }); +} + +class AnalyticsCacheModel extends CacheEntry { + ChartAnalyticsModel chartAnalyticsModel; + + AnalyticsCacheModel({ + required this.chartAnalyticsModel, + required super.timeSpan, + required super.defaultSelected, + super.selected, + }); + + @override + bool get isExpired => + DateTime.now().difference(_createdAt).inMinutes > + ClassDefaultValues.minutesDelayToMakeNewChartAnalytics; } diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index e04f0a381..c9c9def40 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,40 +1,188 @@ import 'dart:async'; import 'dart:developer'; +import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/analytics_event.dart'; -import 'package:fluffychat/pangea/models/constructs_event.dart'; -import 'package:fluffychat/pangea/models/constructs_model.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_event.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_model.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import '../extensions/client_extension/client_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -import '../models/constructs_analytics_model.dart'; +// controls the sending of analytics events class MyAnalyticsController extends BaseController { late PangeaController _pangeaController; + Timer? _updateTimer; + final int _maxMessagesCached = 10; + final int _minutesBeforeUpdate = 5; MyAnalyticsController(PangeaController pangeaController) { _pangeaController = pangeaController; } - final List analyticsEventTypes = [ - PangeaEventTypes.summaryAnalytics, - PangeaEventTypes.construct, - ]; + // adds the listener that handles when to run automatic updates + // to analytics - either after a certain number of messages sent/ + // received or after a certain amount of time without an update + Future addEventsListener() async { + final Client client = _pangeaController.matrixState.client; - Future sendAllAnalyticsEvents( + // if analytics haven't been updated in the last day, update them + DateTime? lastUpdated = await _pangeaController.analytics + .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); + final DateTime yesterday = DateTime.now().subtract(const Duration(days: 1)); + if (lastUpdated?.isBefore(yesterday) ?? false) { + debugPrint("analytics out-of-date, updating"); + await updateAnalytics(); + lastUpdated = await _pangeaController.analytics + .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); + } + + client.onSync.stream + .where((SyncUpdate update) => update.rooms?.join != null) + .listen((update) { + updateAnalyticsTimer(update, lastUpdated); + }); + } + + // given an update from sync stream, check if the update contains + // messages for which analytics will be saved. If so, reset the timer + // and add the event ID to the cache of un-added event IDs + void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) { + for (final entry in update.rooms!.join!.entries) { + final Room room = + _pangeaController.matrixState.client.getRoomById(entry.key)!; + + // get the new events in this sync that are messages + final List? events = entry.value.timeline?.events + ?.map((event) => Event.fromMatrixEvent(event, room)) + .where((event) => eventHasAnalytics(event, lastUpdated)) + .toList(); + + // add their event IDs to the cache of un-added event IDs + if (events == null || events.isEmpty) continue; + for (final event in events) { + addMessageSinceUpdate(event.eventId); + } + + // cancel the last timer that was set on message event and + // reset it to fire after _minutesBeforeUpdate minutes + _updateTimer?.cancel(); + _updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () { + debugPrint("timer fired, updating analytics"); + updateAnalytics(); + }); + } + } + + // checks if event from sync update is a message that should have analytics + bool eventHasAnalytics(Event event, DateTime? lastUpdated) { + return event.originServerTs.isAfter(lastUpdated ?? DateTime.now()) && + event.type == EventTypes.Message && + event.messageType == MessageTypes.Text && + !(event.eventId.contains("web") && + !(event.eventId.contains("android")) && + !(event.eventId.contains("iOS"))); + } + + // adds an event ID to the cache of un-added event IDs + // if the event IDs isn't already added + void addMessageSinceUpdate(String eventId) { + final List currentCache = messagesSinceUpdate; + if (!currentCache.contains(eventId)) { + currentCache.add(eventId); + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + currentCache, + local: true, + ); + } + + // if the cached has reached if max-length, update analytics + if (messagesSinceUpdate.length > _maxMessagesCached) { + debugPrint("reached max messages, updating"); + updateAnalytics(); + } + } + + // called before updating analytics + void clearMessagesSinceUpdate() { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + } + + // a local cache of eventIds for messages sent since the last update + // it's possible for this cache to be invalid or deleted + // It's a proxy measure for messages sent since last update + List get messagesSinceUpdate { + final dynamic locallySaved = _pangeaController.pStoreService.read( + PLocalKey.messagesSinceUpdate, + local: true, + ); + if (locallySaved == null) { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + return []; + } + + try { + return locallySaved as List; + } catch (err) { + _pangeaController.pStoreService.save( + PLocalKey.messagesSinceUpdate, + [], + local: true, + ); + return []; + } + } + + Future updateAnalytics() async { + // top level analytics sending function. Send analytics + // for each type of analytics event + // to each of the applicable analytics rooms + clearMessagesSinceUpdate(); + + // fetch a list of all the chats that the user is studying + // and a list of all the spaces in which the user is studying + await setStudentChats(); + await setStudentSpaces(); + + // get all the analytics rooms that the user has + // and create any missing analytics rooms (if the user is studying + // in a class but doesn't have an analytics room for that class's L2) + final List analyticsRooms = + _pangeaController.matrixState.client.allMyAnalyticsRooms; + analyticsRooms.addAll(await createMissingAnalyticsRooms()); + + // finally, send an analytics event for each analytics room and + // each type of analytics event + for (final Room analyticsRoom in analyticsRooms) { + for (final String type in AnalyticsEvent.analyticsEventTypes) { + await sendAnalyticsEvent(analyticsRoom, type); + } + } + } + + Future sendAnalyticsEvent( Room analyticsRoom, + String type, ) async { + // given an analytics room for a language and a type of analytics event + // gathers all the relevant data and sends it to the analytics room + + // get the language code for the analytics room final String? langCode = analyticsRoom.madeForLang; if (langCode == null) { ErrorHandler.logError( @@ -44,175 +192,78 @@ class MyAnalyticsController extends BaseController { return; } - final Map prevEvents = {}; - for (final type in analyticsEventTypes) { - final prevEvent = await analyticsRoom.getLastAnalyticsEvent(type); - prevEvents[type] = prevEvent; - } + // get the last time an analytics event of this type was sent to this room + final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated( + type, + _pangeaController.matrixState.client.userID!, + ); - final List lastUpdates = prevEvents.values - .map((e) => e?.content.lastUpdated) - .cast() - .toList(); - - DateTime? earliestLastUpdated; - if (!lastUpdates.any((updated) => updated == null)) { - earliestLastUpdated = lastUpdates.reduce( - (min, e) => e!.isBefore(min!) ? e : min, - ); - } - - final List analyticsContent = []; - final List constructsContent = []; + // each type of analytics event has a format for storing per-message data + // for SummaryAnalytics events, this is RecentMessageRecord + // for Construct events, this is OneConstructUse + // analyticsContent is a list of these formatted data + final List analyticsContent = []; for (final Room chat in _studentChats) { + // for each chat the student studies in, check if the langCode + // matches the langCode of the analytics room final String? chatLangCode = _pangeaController.languageController.activeL2Code(roomID: chat.id); if (chatLangCode != langCode) continue; - final List recentMsgs = - await chat.myMessageEventsInChat( - since: earliestLastUpdated, - ); - - analyticsContent.addAll( - formatAnalyticsContent( - recentMsgs, - prevEvents[PangeaEventTypes.summaryAnalytics] - as SummaryAnalyticsEvent?, - ), - ); - - constructsContent.addAll( - formatConstructsContent( - recentMsgs, - prevEvents[PangeaEventTypes.construct] as ConstructAnalyticsEvent?, - ), - ); - } - - if (analyticsContent.isNotEmpty) { - final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( - messages: analyticsContent, - lastUpdated: DateTime.now(), - prevEventId: - prevEvents[PangeaEventTypes.summaryAnalytics]?.event.eventId, - prevLastUpdated: - prevEvents[PangeaEventTypes.summaryAnalytics]?.content.lastUpdated, - ); - await analyticsRoom.sendEvent( - analyticsModel.toJson(), - type: PangeaEventTypes.summaryAnalytics, - ); - } - - if (constructsContent.isNotEmpty) { - final Map> lemmasUses = {}; - for (final use in constructsContent) { - if (use.lemma == null) { - debugPrint("use has no lemma!"); - continue; - } - lemmasUses[use.lemma!] ??= []; - lemmasUses[use.lemma]!.add(use); + // get messages the logged in user has sent in all chats + // since the last analytics event was sent + List? recentMsgs; + try { + recentMsgs = await chat.myMessageEventsInChat( + since: lastUpdated, + ); + } catch (err) { + debugPrint("failed to fetch messages for chat ${chat.id}"); + continue; } - final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( - type: ConstructType.grammar, - uses: lemmasUses.entries - .map( - (entry) => LemmaConstructsModel( - lemma: entry.key, - uses: entry.value, - ), - ) - .toList(), - lastUpdated: DateTime.now(), - prevEventId: prevEvents[PangeaEventTypes.construct]?.event.eventId, - prevLastUpdated: - prevEvents[PangeaEventTypes.construct]?.content.lastUpdated, - ); + if (lastUpdated != null) { + recentMsgs.removeWhere( + (msg) => msg.event.originServerTs.isBefore(lastUpdated), + ); + } - await analyticsRoom.sendEvent( - constructsModel.toJson(), - type: PangeaEventTypes.construct, + // then format that data into analytics data and add the formatted + // data to the list of analyticsContent + analyticsContent.addAll( + AnalyticsModel.formatAnalyticsContent(recentMsgs, type), ); } + + // send the analytics data to the analytics room + if (analyticsContent.isEmpty) return; + await AnalyticsEvent.sendEvent( + analyticsRoom, + type, + analyticsContent, + ); } - List formatAnalyticsContent( - List recentMsgs, - SummaryAnalyticsEvent? prevEvent, - ) { - List filtered = List.from(recentMsgs); - if (prevEvent?.content.lastUpdated != null) { - filtered = recentMsgs - .where( - (msg) => msg.event.originServerTs.isAfter( - prevEvent!.content.lastUpdated!, - ), - ) - .toList(); - } - - final List addedMsgIds = - prevEvent?.content.messages.map((msg) => msg.eventId).toList() ?? []; - - filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId)); - - final List records = filtered + // on the off chance that the user is in a class but doesn't have an analytics + // room for the target language of that class, create the analytics room(s) + Future> createMissingAnalyticsRooms() async { + List targetLangs = []; + final String? userL2 = _pangeaController.languageController.activeL2Code(); + if (userL2 != null) targetLangs.add(userL2); + final List spaceL2s = studentSpaces .map( - (msg) => RecentMessageRecord( - eventId: msg.eventId, - chatId: msg.room.id, - useType: msg.useType, - time: msg.originServerTs, + (space) => _pangeaController.languageController.activeL2Code( + roomID: space.id, ), ) .toList(); - - return records; - } - - List formatConstructsContent( - List recentMsgs, - ConstructAnalyticsEvent? prevEvent, - ) { - List filtered = List.from(recentMsgs); - if (prevEvent?.content.lastUpdated != null) { - filtered = recentMsgs - .where( - (msg) => msg.event.originServerTs.isAfter( - prevEvent!.content.lastUpdated!, - ), - ) - .toList(); + targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast()); + targetLangs = targetLangs.toSet().toList(); + for (final String langCode in targetLangs) { + await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode); } - - final List addedMsgIds = prevEvent?.content.uses - .map((lemmause) => lemmause.uses.map((use) => use.msgId)) - .expand((element) => element) - .where((element) => element != null) - .cast() - .toList() ?? - []; - - filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId)); - - final List uses = filtered - .map( - (msg) => msg.originalSent?.choreo?.toGrammarConstructUse( - msg.eventId, - msg.room.id, - msg.originServerTs, - ), - ) - .where((element) => element != null) - .cast>() - .expand((element) => element) - .toList(); - - return uses; + return _pangeaController.matrixState.client.allMyAnalyticsRooms; } List _studentChats = []; @@ -259,103 +310,4 @@ class MyAnalyticsController extends BaseController { return []; } } - - // on the off chance that the user is in a class but doesn't have an analytics - // room for the target language of that class, create the analytics room(s) - Future> createMissingAnalyticsRoom() async { - List targetLangs = []; - final String? userL2 = _pangeaController.languageController.activeL2Code(); - if (userL2 != null) targetLangs.add(userL2); - final List spaceL2s = studentSpaces - .map( - (space) => _pangeaController.languageController.activeL2Code( - roomID: space.id, - ), - ) - .toList(); - targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast()); - targetLangs = targetLangs.toSet().toList(); - for (final String langCode in targetLangs) { - await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode); - } - return _pangeaController.matrixState.client.allMyAnalyticsRooms; - } - - Future updateAnalytics() async { - await setStudentChats(); - await setStudentSpaces(); - final List analyticsRooms = - _pangeaController.matrixState.client.allMyAnalyticsRooms; - analyticsRooms.addAll(await createMissingAnalyticsRoom()); - - for (final Room analyticsRoom in analyticsRooms) { - await sendAllAnalyticsEvents(analyticsRoom); - } - } - - // used to aggregate ConstructEvents, from multiple senders (students) with the same lemma - List aggregateConstructData( - List constructs, - ) { - final Map> lemmasToConstructs = {}; - for (final construct in constructs) { - for (final lemmaUses in construct.content.uses) { - lemmasToConstructs[lemmaUses.lemma] ??= []; - lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses); - } - } - - final List aggregatedConstructs = []; - for (final lemmaToConstructs in lemmasToConstructs.entries) { - final List lemmaConstructs = - lemmaToConstructs.value; - final AggregateConstructUses aggregatedData = AggregateConstructUses( - lemmaUses: lemmaConstructs, - ); - aggregatedConstructs.add(aggregatedData); - } - return aggregatedConstructs; - } - - Future analyticsLastUpdated(String type) async { - final List analyticsRooms = - _pangeaController.matrixState.client.allMyAnalyticsRooms; - if (analyticsRooms.isEmpty) return null; - final List lastUpdates = []; - for (final analyticsRoom in analyticsRooms) { - final AnalyticsEvent? lastEvent = - await analyticsRoom.getLastAnalyticsEvent( - type, - ); - if (lastEvent?.content.lastUpdated != null) { - lastUpdates.add(lastEvent!.content.lastUpdated!); - } - } - if (lastUpdates.isEmpty) return null; - return lastUpdates.reduce( - (value, element) => value.isAfter(element) ? value : element, - ); - } -} - -class AggregateConstructUses { - final List _lemmaUses; - - AggregateConstructUses({required List lemmaUses}) - : _lemmaUses = lemmaUses; - - String get lemma { - assert( - _lemmaUses.isNotEmpty && - _lemmaUses.every( - (construct) => construct.lemma == _lemmaUses.first.lemma, - ), - ); - return _lemmaUses.first.lemma; - } - - List get uses => _lemmaUses - .map((lemmaUse) => lemmaUse.uses) - .expand((element) => element) - .toList(); } diff --git a/lib/pangea/enum/time_span.dart b/lib/pangea/enum/time_span.dart index 23a54e4ea..ddc9ce32b 100644 --- a/lib/pangea/enum/time_span.dart +++ b/lib/pangea/enum/time_span.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../models/chart_analytics_model.dart'; +import '../models/analytics/chart_analytics_model.dart'; enum TimeSpan { day, week, month, sixmonths, year } diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index b91e8afe1..feb71aa4a 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -7,7 +7,6 @@ extension EventsRoomExtension on Room { required String type, }) async { try { - debugPrint("creating $type child for $parentEventId"); Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); if (parentEventId.contains("web")) { debugger(when: kDebugMode); @@ -268,58 +267,82 @@ extension EventsRoomExtension on Room { Future> myMessageEventsInChat({ DateTime? since, + }) async { + final List msgEvents = await getEventsBySender( + type: EventTypes.Message, + sender: client.userID!, + since: since, + ); + final Timeline timeline = await getTimeline(); + return msgEvents + .where((event) => (event.content['msgtype'] == MessageTypes.Text)) + .map((event) { + return PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + ); + }).toList(); + } + + // fetch event of a certain type by a certain sender + // since a certain time or up to a certain amount + Future> getEventsBySender({ + required String type, + required String sender, + DateTime? since, + int? count, }) async { try { int numberOfSearches = 0; - if (isSpace) { - throw Exception( - "In messageListForChat with room that is not a chat", - ); - } final Timeline timeline = await getTimeline(); - while (timeline.canRequestHistory && numberOfSearches < 50) { - try { - await timeline.requestHistory(); - } catch (err) { - break; + + List relevantEvents() => timeline.events + .where((event) => event.senderId == sender && event.type == type) + .toList(); + + bool reachedEnd() { + if (since != null) { + return relevantEvents().any( + (event) => event.originServerTs.isBefore(since), + ); } + if (count != null) { + return relevantEvents().length >= count; + } + return false; + } + + while (timeline.canRequestHistory && + !reachedEnd() && + numberOfSearches < 10) { + await timeline.requestHistory(historyCount: 100); numberOfSearches += 1; - if (timeline.events.any( - (event) => event.originServerTs.isAfter(since ?? DateTime.now()), - )) { + if (reachedEnd()) { break; } } - final List msgs = []; - for (Event event in timeline.events) { - final bool hasAnalytics = (event.senderId == client.userID) && - (event.type == EventTypes.Message) && - (event.content['msgtype'] == MessageTypes.Text && - !(event.relationshipType == RelationshipTypes.edit)); - if (hasAnalytics && - (since == null || event.originServerTs.isAfter(since))) { - if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) { - event = event - .aggregatedEvents( - timeline, - RelationshipTypes.edit, - ) - .sorted( - (a, b) => b.originServerTs.compareTo(a.originServerTs), - ) - .firstOrNull ?? - event; - } - final PangeaMessageEvent pMsgEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, - ); - msgs.add(pMsgEvent); - } + final List fetchedEvents = timeline.events + .where((event) => event.senderId == sender && event.type == type) + .toList(); + + if (since != null) { + fetchedEvents.removeWhere( + (event) => event.originServerTs.isBefore(since), + ); } - return msgs; + + final List events = []; + for (Event event in fetchedEvents) { + if (event.relationshipType == RelationshipTypes.edit) continue; + if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) { + event = event.getDisplayEvent(timeline); + } + events.add(event); + } + + return events; } catch (err, s) { if (kDebugMode) rethrow; debugger(when: kDebugMode); diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 4b68992d3..166ab179c 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -6,11 +6,12 @@ import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; -import 'package:fluffychat/pangea/models/constructs_event.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -28,7 +29,6 @@ import '../../constants/pangea_event_types.dart'; import '../../enum/use_type.dart'; import '../../models/choreo_record.dart'; import '../../models/representation_content_model.dart'; -import '../../models/student_analytics_summary_model.dart'; import '../client_extension/client_extension.dart'; part "children_and_parents_extension.dart"; @@ -69,19 +69,20 @@ extension PangeaRoom on Room { Future getLastAnalyticsEvent( String type, + String userId, ) async => - await _getLastAnalyticsEvent(type); + await _getLastAnalyticsEvent(type, userId); - Future getPrevAnalyticsEvent( - AnalyticsEvent analyticsEvent, - ) async => - await _getPrevAnalyticsEvent(analyticsEvent); + Future analyticsLastUpdated(String type, String userId) async { + return await _analyticsLastUpdated(type, userId); + } Future?> getAnalyticsEvents({ required String type, + required String userId, DateTime? since, }) async => - await _getAnalyticsEvents(type: type, since: since); + await _getAnalyticsEvents(type: type, since: since, userId: userId); String? get madeForLang => _madeForLang; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 3fa2bc247..756f83adf 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -197,77 +197,53 @@ extension AnalyticsRoomExtension on Room { Future _getLastAnalyticsEvent( String type, + String userId, ) async { - final Timeline timeline = await getTimeline(); - int requests = 0; - Event? lastEvent = timeline.events.firstWhereOrNull( - (event) => event.type == type, + final List events = await getEventsBySender( + type: type, + sender: userId, + count: 1, ); - - while (requests < 10 && timeline.canRequestHistory && lastEvent == null) { - await timeline.requestHistory(); - lastEvent = timeline.events.firstWhereOrNull( - (event) => event.type == type, - ); - requests++; - } - - if (lastEvent == null) return null; - + if (events.isEmpty) return null; + final Event event = events.first; + AnalyticsEvent? analyticsEvent; switch (type) { case PangeaEventTypes.summaryAnalytics: - return SummaryAnalyticsEvent(event: lastEvent); + analyticsEvent = SummaryAnalyticsEvent(event: event); case PangeaEventTypes.construct: - return ConstructAnalyticsEvent(event: lastEvent); + analyticsEvent = ConstructAnalyticsEvent(event: event); } - - return null; + return analyticsEvent; } - Future _getPrevAnalyticsEvent( - AnalyticsEvent analyticsEvent, - ) async { - if (analyticsEvent.content.prevEventId == null) { - return null; - } - final Event? prevEvent = await getEventById( - analyticsEvent.content.prevEventId!, - ); - if (prevEvent == null) return null; - - switch (analyticsEvent.event.type) { - case PangeaEventTypes.summaryAnalytics: - return SummaryAnalyticsEvent(event: prevEvent); - case PangeaEventTypes.construct: - return ConstructAnalyticsEvent(event: prevEvent); - } - - return null; + Future _analyticsLastUpdated(String type, String userId) async { + final lastEvent = await _getLastAnalyticsEvent(type, userId); + return lastEvent?.event.originServerTs; } Future?> _getAnalyticsEvents({ required String type, + required String userId, DateTime? since, }) async { - final AnalyticsEvent? mostRecentEvent = await getLastAnalyticsEvent(type); - if (mostRecentEvent == null) return null; - final List events = [mostRecentEvent]; - - bool getAllEvents() => - since == null && events.last.content.prevEventId == null; - - bool reachedUpdated() => - since != null && - (events.last.content.lastUpdated?.isBefore(since) ?? true); - - while (getAllEvents() || !reachedUpdated()) { - final AnalyticsEvent? prevEvent = await getPrevAnalyticsEvent( - events.last, - ); - if (prevEvent == null) break; - events.add(prevEvent); + final List events = await getEventsBySender( + type: type, + sender: userId, + since: since, + ); + final List analyticsEvents = []; + for (final Event event in events) { + switch (type) { + case PangeaEventTypes.summaryAnalytics: + analyticsEvents.add(SummaryAnalyticsEvent(event: event)); + break; + case PangeaEventTypes.construct: + analyticsEvents.add(ConstructAnalyticsEvent(event: event)); + break; + } } - return events; + + return analyticsEvents; } String? get _madeForLang { diff --git a/lib/pangea/models/analytics/analytics_event.dart b/lib/pangea/models/analytics/analytics_event.dart new file mode 100644 index 000000000..2453e62ef --- /dev/null +++ b/lib/pangea/models/analytics/analytics_event.dart @@ -0,0 +1,59 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; +import 'package:matrix/matrix.dart'; + +// superclass for all analytics events +abstract class AnalyticsEvent { + late Event _event; + AnalyticsModel? contentCache; + + AnalyticsEvent({required Event event}) { + _event = event; + } + + Event get event => _event; + + AnalyticsModel get content { + switch (_event.type) { + case PangeaEventTypes.summaryAnalytics: + contentCache ??= SummaryAnalyticsModel.fromJson(event.content); + break; + case PangeaEventTypes.construct: + contentCache ??= ConstructAnalyticsModel.fromJson(event.content); + break; + } + return contentCache!; + } + + static List analyticsEventTypes = [ + PangeaEventTypes.summaryAnalytics, + PangeaEventTypes.construct, + ]; + + static Future sendEvent( + Room analyticsRoom, + String type, + List analyticsContent, + ) async { + String? eventId; + switch (type) { + case PangeaEventTypes.summaryAnalytics: + eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( + analyticsRoom, + analyticsContent.cast(), + ); + break; + case PangeaEventTypes.construct: + eventId = await ConstructAnalyticsEvent.sendConstructsEvent( + analyticsRoom, + analyticsContent.cast(), + ); + break; + } + return eventId; + } +} diff --git a/lib/pangea/models/analytics/analytics_model.dart b/lib/pangea/models/analytics/analytics_model.dart new file mode 100644 index 000000000..bdb3bc6d5 --- /dev/null +++ b/lib/pangea/models/analytics/analytics_model.dart @@ -0,0 +1,19 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; + +abstract class AnalyticsModel { + static List formatAnalyticsContent( + List recentMsgs, + String type, + ) { + switch (type) { + case PangeaEventTypes.summaryAnalytics: + return SummaryAnalyticsModel.formatSummaryContent(recentMsgs); + case PangeaEventTypes.construct: + return ConstructAnalyticsModel.formatConstructsContent(recentMsgs); + } + return []; + } +} diff --git a/lib/pangea/models/chart_analytics_model.dart b/lib/pangea/models/analytics/chart_analytics_model.dart similarity index 96% rename from lib/pangea/models/chart_analytics_model.dart rename to lib/pangea/models/analytics/chart_analytics_model.dart index af06a0958..651ec1152 100644 --- a/lib/pangea/models/chart_analytics_model.dart +++ b/lib/pangea/models/analytics/chart_analytics_model.dart @@ -1,10 +1,10 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:flutter/foundation.dart'; -import 'package:fluffychat/pangea/enum/time_span.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; -import '../enum/use_type.dart'; +import '../../enum/use_type.dart'; class TimeSeriesTotals { int ta; diff --git a/lib/pangea/models/analytics/constructs_event.dart b/lib/pangea/models/analytics/constructs_event.dart new file mode 100644 index 000000000..d297ba893 --- /dev/null +++ b/lib/pangea/models/analytics/constructs_event.dart @@ -0,0 +1,65 @@ +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:matrix/matrix.dart'; + +import '../../constants/pangea_event_types.dart'; + +class ConstructAnalyticsEvent extends AnalyticsEvent { + ConstructAnalyticsEvent({required Event event}) : super(event: event) { + if (event.type != PangeaEventTypes.construct) { + throw Exception( + "${event.type} should not be used to make a ConstructAnalyticsEvent", + ); + } + } + + @override + ConstructAnalyticsModel get content { + contentCache ??= ConstructAnalyticsModel.fromJson(event.content); + return contentCache as ConstructAnalyticsModel; + } + + static Future sendConstructsEvent( + Room analyticsRoom, + List uses, + ) async { + // create a map of lemmas to their uses + final Map> lemmasToUses = {}; + for (final use in uses) { + if (use.lemma == null) { + ErrorHandler.logError( + e: "use has no lemma in sendConstructsEvent", + s: StackTrace.current, + ); + continue; + } + lemmasToUses[use.lemma!] ??= []; + lemmasToUses[use.lemma]!.add(use); + } + + // convert the map of lemmas to uses into a list of LemmaConstructsModel + // each entry in this list contains one lemma to many uses + final List lemmaUses = lemmasToUses.entries + .map( + (entry) => LemmaConstructsModel( + lemma: entry.key, + uses: entry.value, + ), + ) + .toList(); + + // finally, send the construct analytics event to the analytics room + final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( + type: ConstructType.grammar, + uses: lemmaUses, + ); + + final String? eventId = await analyticsRoom.sendEvent( + constructsModel.toJson(), + type: PangeaEventTypes.construct, + ); + return eventId; + } +} diff --git a/lib/pangea/models/constructs_analytics_model.dart b/lib/pangea/models/analytics/constructs_model.dart similarity index 67% rename from lib/pangea/models/constructs_analytics_model.dart rename to lib/pangea/models/analytics/constructs_model.dart index b45da25e6..a16855c2b 100644 --- a/lib/pangea/models/constructs_analytics_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,70 +1,94 @@ -import 'dart:developer'; - import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:flutter/foundation.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import '../enum/construct_type_enum.dart'; +import '../../enum/construct_type_enum.dart'; -class ConstructUses { - String lemma; +class ConstructAnalyticsModel extends AnalyticsModel { ConstructType type; + List uses; - List uses; - - //PTODO - how to incorporate semantic similarity score into this? - - //PTODO - add variables for saving requests for - // 1) definitions - // 2) translations - // 3) examples??? (gpt suggested) - - ConstructUses({ - required this.lemma, + ConstructAnalyticsModel({ required this.type, this.uses = const [], }); - factory ConstructUses.fromJson(Map json) { - // try { - debugger( - when: - kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null), + static const _usesKey = "uses"; + + factory ConstructAnalyticsModel.fromJson(Map json) { + return ConstructAnalyticsModel( + type: ConstructTypeUtil.fromString(json['type']), + uses: json[_usesKey] + .values + .map((lemmaUses) => LemmaConstructsModel.fromJson(lemmaUses)) + .cast() + .toList(), ); - return ConstructUses( + } + + toJson() { + final Map usesMap = {}; + for (final use in uses) { + usesMap[use.lemma] = use.toJson(); + } + + return { + 'type': type.string, + _usesKey: usesMap, + }; + } + + static List formatConstructsContent( + List recentMsgs, + ) { + final List filtered = List.from(recentMsgs); + final List uses = filtered + .map( + (msg) => msg.originalSent?.choreo?.toGrammarConstructUse( + msg.eventId, + msg.room.id, + msg.originServerTs, + ), + ) + .where((element) => element != null) + .cast>() + .expand((element) => element) + .toList(); + + return uses; + } +} + +class LemmaConstructsModel { + String lemma; + List uses; + + LemmaConstructsModel({ + required this.lemma, + this.uses = const [], + }); + + factory LemmaConstructsModel.fromJson(Map json) { + return LemmaConstructsModel( lemma: json[ModelKey.lemma], - uses: (json['uses'] as Iterable) + uses: (json['uses'] ?? [] as Iterable) .map( (use) => use != null ? OneConstructUse.fromJson(use) : null, ) .where((element) => element != null) .cast() .toList(), - type: ConstructTypeUtil.fromString(json['type']), ); - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } } - toJson() { + Map toJson() { return { ModelKey.lemma: lemma, 'uses': uses.map((use) => use.toJson()).toList(), - 'type': type.string, }; } - - void addUsesByUseType(List uses) { - for (final use in uses) { - if (use.lemma != lemma) { - throw Exception('lemma mismatch'); - } - uses.add(use); - } - } } enum ConstructUseType { @@ -209,3 +233,25 @@ class OneConstructUse { return room.getEventById(msgId!); } } + +class AggregateConstructUses { + final List _lemmaUses; + + AggregateConstructUses({required List lemmaUses}) + : _lemmaUses = lemmaUses; + + String get lemma { + assert( + _lemmaUses.isNotEmpty && + _lemmaUses.every( + (construct) => construct.lemma == _lemmaUses.first.lemma, + ), + ); + return _lemmaUses.first.lemma; + } + + List get uses => _lemmaUses + .map((lemmaUse) => lemmaUse.uses) + .expand((element) => element) + .toList(); +} diff --git a/lib/pangea/models/analytics/summary_analytics_event.dart b/lib/pangea/models/analytics/summary_analytics_event.dart new file mode 100644 index 000000000..e7034eaa4 --- /dev/null +++ b/lib/pangea/models/analytics/summary_analytics_event.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; +import 'package:matrix/matrix.dart'; + +import '../../constants/pangea_event_types.dart'; + +class SummaryAnalyticsEvent extends AnalyticsEvent { + SummaryAnalyticsEvent({required Event event}) : super(event: event) { + if (event.type != PangeaEventTypes.summaryAnalytics) { + throw Exception( + "${event.type} should not be used to make a SummaryAnalyticsEvent", + ); + } + } + + @override + SummaryAnalyticsModel get content { + contentCache ??= SummaryAnalyticsModel.fromJson(event.content); + return contentCache as SummaryAnalyticsModel; + } + + static Future sendSummaryAnalyticsEvent( + Room analyticsRoom, + List records, + ) async { + final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( + messages: records, + ); + final String? eventId = await analyticsRoom.sendEvent( + analyticsModel.toJson(), + type: PangeaEventTypes.summaryAnalytics, + ); + return eventId; + } +} diff --git a/lib/pangea/models/student_analytics_summary_model.dart b/lib/pangea/models/analytics/summary_analytics_model.dart similarity index 72% rename from lib/pangea/models/student_analytics_summary_model.dart rename to lib/pangea/models/analytics/summary_analytics_model.dart index 7ed617d91..b09d0a870 100644 --- a/lib/pangea/models/student_analytics_summary_model.dart +++ b/lib/pangea/models/analytics/summary_analytics_model.dart @@ -1,10 +1,64 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import 'package:matrix/matrix.dart'; -import '../enum/use_type.dart'; +class SummaryAnalyticsModel extends AnalyticsModel { + late List _messages; + + SummaryAnalyticsModel({ + required List messages, + }) { + _messages = messages; + } + + List get messages => _messages; + + static const _messagesKey = "msgs"; + + Map toJson() => { + _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), + }; + + factory SummaryAnalyticsModel.fromJson(json) { + List savedMessages = []; + try { + savedMessages = json[_messagesKey] != null + ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) + .map((e) => RecentMessageRecord.fromJson(e)) + .toList() + .cast() + : []; + } catch (err, stack) { + if (kDebugMode) rethrow; + ErrorHandler.logError(e: err, s: stack); + } + return SummaryAnalyticsModel( + messages: savedMessages, + ); + } + + static List formatSummaryContent( + List recentMsgs, + ) { + final List filtered = List.from(recentMsgs); + final List records = filtered + .map( + (msg) => RecentMessageRecord( + eventId: msg.eventId, + chatId: msg.room.id, + useType: msg.useType, + time: msg.originServerTs, + ), + ) + .toList(); + + return records; + } +} class RecentMessageRecord { String eventId; @@ -55,60 +109,3 @@ class RecentMessageRecord { static const _typeOfUseKey = "typ"; static const _timeKey = "t"; } - -class StudentAnalyticsSummary { - late List _messages; - DateTime? lastUpdated; - - StudentAnalyticsSummary({ - required List messages, - required this.lastUpdated, - }) { - _messages = messages; - } - - void addAll(List msgs) { - for (final msg in msgs) { - if (!(_messages.any((element) => element.eventId == msg.eventId))) { - _messages.add(msg); - } - } - } - - void removeEdittedMessages(Client client, List removeEventIds) { - _messages.removeWhere( - (element) => removeEventIds.contains(element.eventId), - ); - } - - List get messages => _messages; - - static const _messagesKey = "msgs"; - static const _lastUpdatedKey = "lupt"; - - Map toJson() => { - _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), - _lastUpdatedKey: lastUpdated?.toIso8601String(), - }; - - factory StudentAnalyticsSummary.fromJson(json) { - List savedMessages = []; - try { - savedMessages = json[_messagesKey] != null - ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) - .map((e) => RecentMessageRecord.fromJson(e)) - .toList() - .cast() - : []; - } catch (err, stack) { - if (kDebugMode) rethrow; - ErrorHandler.logError(e: err, s: stack); - } - return StudentAnalyticsSummary( - messages: savedMessages, - lastUpdated: json[_lastUpdatedKey] != null - ? DateTime.parse(json[_lastUpdatedKey]) - : null, - ); - } -} diff --git a/lib/pangea/models/analytics_event.dart b/lib/pangea/models/analytics_event.dart deleted file mode 100644 index 0887354cf..000000000 --- a/lib/pangea/models/analytics_event.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/models/analytics_model.dart'; -import 'package:fluffychat/pangea/models/constructs_model.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_model.dart'; -import 'package:matrix/matrix.dart'; - -abstract class AnalyticsEvent { - late Event _event; - AnalyticsModel? contentCache; - - AnalyticsEvent({required Event event}) { - _event = event; - } - - Event get event => _event; - - AnalyticsModel get content { - switch (_event.type) { - case PangeaEventTypes.summaryAnalytics: - contentCache ??= SummaryAnalyticsModel.fromJson(event.content); - break; - case PangeaEventTypes.construct: - contentCache ??= ConstructAnalyticsModel.fromJson(event.content); - break; - } - return contentCache!; - } -} diff --git a/lib/pangea/models/analytics_model.dart b/lib/pangea/models/analytics_model.dart deleted file mode 100644 index 7e376aa4e..000000000 --- a/lib/pangea/models/analytics_model.dart +++ /dev/null @@ -1,11 +0,0 @@ -abstract class AnalyticsModel { - DateTime? lastUpdated; - String? prevEventId; - DateTime? prevLastUpdated; - - AnalyticsModel({ - this.lastUpdated, - this.prevEventId, - this.prevLastUpdated, - }); -} diff --git a/lib/pangea/models/analytics_model_old.dart b/lib/pangea/models/analytics_model_old.dart deleted file mode 100644 index 8dc5159da..000000000 --- a/lib/pangea/models/analytics_model_old.dart +++ /dev/null @@ -1,100 +0,0 @@ -// import 'dart:convert'; - -// class UserTimeSeriesInterval { -// String? userId; -// int? taTotal; -// int? gaTotal; -// int? waTotal; - -// UserTimeSeriesInterval({ -// required this.userId, -// required this.taTotal, -// required this.gaTotal, -// required this.waTotal, -// }); - -// Map toJson() => -// {"usr": userId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; - -// factory UserTimeSeriesInterval.fromJson(json) => UserTimeSeriesInterval( -// userId: json["usr"], -// taTotal: json["ta"], -// gaTotal: json["ga"], -// waTotal: json["wa"], -// ); -// } - -// class TimeSeriesInterval { -// DateTime start; -// DateTime end; -// List users; - -// TimeSeriesInterval({ -// required this.start, -// required this.end, -// required this.users, -// }); - -// Map toJson() => { -// "strt": start, -// "end": end, -// "usrs": jsonEncode(users.map((e) => e.toJson()).toList()) -// }; - -// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( -// start: json["strt"], -// end: json["end"], -// users: ((jsonDecode(json["usrs"]) as Iterable) -// .map((e) => UserTimeSeriesInterval.fromJson(e)) -// .toList() -// .cast()), -// ); -// } - -// class RoomAnalyticsSummary { -// List monthlyTotalsForAllTime; -// List dailyTotalsForLast30Days; -// List hourlyTotalsForLast24Hours; - -// DateTime? updatedAt; - -// RoomAnalyticsSummary({ -// required this.monthlyTotalsForAllTime, -// required this.dailyTotalsForLast30Days, -// required this.hourlyTotalsForLast24Hours, -// }); - -// Map toJson() => { -// "mnths": -// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), -// "dys": jsonEncode( -// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), -// "hrs": jsonEncode( -// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), -// }; - -// factory RoomAnalyticsSummary.fromJson(json) => RoomAnalyticsSummary( -// monthlyTotalsForAllTime: (jsonDecode(json["mnths"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// dailyTotalsForLast30Days: (jsonDecode(json["dys"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// hourlyTotalsForLast24Hours: (jsonDecode(json["hrs"]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// ); -// } - -// class UserDirectChatAnalyticsSummary { -// // directChatRoomIds and analytics for those rooms -// // updated by user; -// Map? directChatSummaries; - -// Map toJson() => {}; -// } - -// // maybe search how to do date ranges in dart diff --git a/lib/pangea/models/analytics_model_older.dart b/lib/pangea/models/analytics_model_older.dart deleted file mode 100644 index 2ee817f0b..000000000 --- a/lib/pangea/models/analytics_model_older.dart +++ /dev/null @@ -1,124 +0,0 @@ -// import 'dart:convert'; - -// class ChatTimeSeriesInterval { -// String? chatId; -// int? taTotal; -// int? gaTotal; -// int? waTotal; - -// ChatTimeSeriesInterval({ -// required this.chatId, -// required this.taTotal, -// required this.gaTotal, -// required this.waTotal, -// }); - -// Map toJson() => -// {"id": chatId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; - -// factory ChatTimeSeriesInterval.fromJson(json) => ChatTimeSeriesInterval( -// chatId: json["id"], -// taTotal: json["ta"], -// gaTotal: json["ga"], -// waTotal: json["wa"], -// ); -// } - -// class TimeSeriesInterval { -// DateTime start; -// DateTime end; -// List chats; - -// TimeSeriesInterval({ -// required this.start, -// required this.end, -// required this.chats, -// }); - -// Map toJson() => { -// "strt": start, -// "end": end, -// "usrs": jsonEncode(chats.map((e) => e.toJson()).toList()) -// }; - -// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( -// start: DateTime(json["strt"]), -// end: DateTime(json["end"]), -// chats: ((jsonDecode(json["usrs"]) as Iterable) -// .map((e) => ChatTimeSeriesInterval.fromJson(e)) -// .toList() -// .cast()), -// ); -// } - -// // class RecentMessageRecord { -// // String eventId; -// // String typeOfUse; -// // String time; -// // } - -// class StudentAnalyticsSummary { -// /// event statekey = studentId -// // String studentId; - -// List monthlyTotalsForAllTime; -// List dailyTotalsForLast30Days; -// List hourlyTotalsForLast24Hours; - -// // List messages; - -// DateTime lastLogin; -// DateTime lastMessage; - -// DateTime lastUpdated; - -// StudentAnalyticsSummary({ -// // required this.studentId, -// required this.monthlyTotalsForAllTime, -// required this.dailyTotalsForLast30Days, -// required this.hourlyTotalsForLast24Hours, -// required this.lastLogin, -// required this.lastMessage, -// required this.lastUpdated, -// }); - -// // static const _studentIdKey = 'usr'; -// static const _monthKey = "mnths"; -// static const _dayKey = "dys"; -// static const _hoursKey = "hrs"; -// static const _lastLoginKey = "lgn"; -// static const _lastMessageKey = "msg"; -// static const _lastUpdated = "lupt"; - -// Map toJson() => { -// _monthKey: -// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), -// _dayKey: jsonEncode( -// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), -// _hoursKey: jsonEncode( -// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), -// // _studentIdKey: studentId, -// _lastLoginKey: lastLogin.toIso8601String(), -// _lastMessageKey: lastMessage.toIso8601String(), -// _lastUpdated: lastUpdated.toIso8601String() -// }; - -// factory StudentAnalyticsSummary.fromJson(json) => StudentAnalyticsSummary( -// // studentId: json[_studentIdKey], -// monthlyTotalsForAllTime: (jsonDecode(json[_monthKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// dailyTotalsForLast30Days: (jsonDecode(json[_dayKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// hourlyTotalsForLast24Hours: (jsonDecode(json[_hoursKey]) as Iterable) -// .map((e) => TimeSeriesInterval.fromJson(e)) -// .toList() -// .cast(), -// lastLogin: DateTime(json[_lastLoginKey]), -// lastUpdated: DateTime(json[_lastLoginKey]), -// lastMessage: DateTime(json[_lastMessageKey]), -// ); -// } diff --git a/lib/pangea/models/analytics_model_oldest.dart b/lib/pangea/models/analytics_model_oldest.dart deleted file mode 100644 index f075c1fe4..000000000 --- a/lib/pangea/models/analytics_model_oldest.dart +++ /dev/null @@ -1,77 +0,0 @@ -// import 'dart:convert'; - -// class BaseDataModel { -// late int spanTotal; -// late int spanIT; -// late int spanIGC; -// late int spanDirect; - -// BaseDataModel(Map json) { -// fromJson(json); -// } - -// fromJson(Map json) { -// spanTotal = json["total"]; -// spanIT = json["it"]; -// spanIGC = json["igc"]; -// spanDirect = json["direct"]; -// } -// } - -// class TimeSeriesInterval extends BaseDataModel { -// //note: always in UTC -// late DateTime start; -// late DateTime end; - -// TimeSeriesInterval(Map json) : super(json) { -// fromJsonTimeSeriesInterval(json); -// } - -// fromJsonTimeSeriesInterval(Map json) { -// start = DateTime.parse(json["start"]); -// end = DateTime.parse(json["end"]); -// } -// } - -// class chartAnalytics extends BaseDataModel { -// late String id; -// late int allTotal; -// late int allIT; -// late int allIGC; -// late int allDirect; -// late String timeSpan; -// late DateTime fetchedAt; -// late List? chatIds; -// late List? userIds; -// late List? classIds; -// late List timeSeries; - -// chartAnalytics(Map json) : super(json) { -// fromJsonchartAnalytics(json); -// fetchedAt = DateTime.now(); -// } - -// fromJsonchartAnalytics(Map json) { -// id = json["id"]; -// timeSpan = json["timespan"]; -// allTotal = json["alltime"]["total"]; -// allIT = json["alltime"]["it"]; -// allIGC = json["alltime"]["igc"]; -// allDirect = json["alltime"]["direct"]; -// timeSeries = (json["timeseries"] as Iterable) -// .map( -// (timeSeriesJsonEntry) => TimeSeriesInterval(timeSeriesJsonEntry), -// ) -// .toList() -// .cast(); -// chatIds = json["chats"] != null && json["chats"] != [] -// ? (json["chats"] as List).cast() -// : null; -// userIds = json["users"] != null && json["userIds"] != [] -// ? (json["users"] as List).cast() -// : null; -// classIds = json["classes"] != null && json["classes"] != [] -// ? (json["classes"] as List).cast() -// : null; -// } -// } diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index 711b30dc2..01acb9994 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,11 +1,11 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import '../constants/choreo_constants.dart'; import '../enum/construct_type_enum.dart'; -import 'constructs_analytics_model.dart'; import 'it_step.dart'; import 'lemma.dart'; diff --git a/lib/pangea/models/class_analytics_model.dart b/lib/pangea/models/class_analytics_model.dart deleted file mode 100644 index ba7f642ce..000000000 --- a/lib/pangea/models/class_analytics_model.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:intl/intl.dart'; - -class ClassAnalyticsModel { - ClassAnalyticsModel(); - late final Null classId; - late final List userIds; - late final List analytics; - get tableView {} - ClassAnalyticsModel.fromJson(Map json) { - classId = null; - userIds = List.castFrom(json['user_ids']); - analytics = - List.from(json['analytics']).map((e) => Analytics.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['class_id'] = classId; - data['user_ids'] = userIds; - data['analytics'] = analytics.map((e) => e.toJson()).toList(); - return data; - } -} - -class Analytics { - Analytics({ - required this.title, - required this.section, - }); - late final String title; - late final List
section; - - Analytics.fromJson(Map json) { - title = json['title']; - section = - List.from(json['section']).map((e) => Section.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['title'] = title; - data['section'] = section.map((e) => e.toJson()).toList(); - return data; - } -} - -class Section { - Section({ - required this.title, - required this.classTotal, - required this.data, - }); - late final String title; - late final String classTotal; - late final List data; - - Section.fromJson(Map json) { - title = json['title']; - classTotal = json['class_total']; - data = List.from(json['data']).map((e) => Data.fromJson(e)).toList(); - } - - Map toJson() { - final data = {}; - data['title'] = title; - data['class_total'] = classTotal; - (data['data'] as List).map((item) => Data.fromJson(item)).toList(); - return data; - } -} - -class Data { - Data(); - set value(String val) => _value = val; - String get value { - if (value_type == 'date') { - return DateFormat('yyyy/M/dd hh:mm a') - .format(DateTime.parse(_value).toLocal()) - .toString(); - } - return _value; - } - - late final String userId; - late final String _value; - late final String value_type; - Data.fromJson(Map json) { - userId = json['user_id']; - _value = json['value']; - value_type = json['value_type']; - } - - Map toJson() { - final data = {}; - data['user_id'] = userId; - data['value'] = _value; - data['value_type'] = value_type; - return data; - } -} diff --git a/lib/pangea/models/constructs_event.dart b/lib/pangea/models/constructs_event.dart deleted file mode 100644 index 2c81bf949..000000000 --- a/lib/pangea/models/constructs_event.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:fluffychat/pangea/models/analytics_event.dart'; -import 'package:fluffychat/pangea/models/constructs_model.dart'; -import 'package:matrix/matrix.dart'; - -import '../constants/pangea_event_types.dart'; - -class ConstructAnalyticsEvent extends AnalyticsEvent { - ConstructAnalyticsEvent({required Event event}) : super(event: event) { - if (event.type != PangeaEventTypes.construct) { - throw Exception( - "${event.type} should not be used to make a ConstructAnalyticsEvent", - ); - } - } - - @override - ConstructAnalyticsModel get content { - contentCache ??= ConstructAnalyticsModel.fromJson(event.content); - return contentCache as ConstructAnalyticsModel; - } - - // void addAll(List uses) { - // for (final use in uses) { - // if (content.uses.any((element) => element.id == use.id)) { - // continue; - // } - // debugPrint("${use.toJson()}"); - // content.uses.add(use); - // } - // event.content = content.toJson(); - // } - - // Future removeEdittedUses( - // List removeIds, - // Client client, - // ) async { - // _contentCache ??= ConstructUses.fromJson(event.content); - // if (_contentCache == null || _event.stateKey == null) return; - // final previousLength = _contentCache!.uses.length; - // _contentCache!.uses.removeWhere( - // (element) => removeIds.contains(element.msgId), - // ); - // if (previousLength > _contentCache!.uses.length) { - // await client.setRoomStateWithKey( - // _event.room.id, - // _event.type, - // _event.stateKey!, - // _contentCache!.toJson(), - // ); - // } - // } -} diff --git a/lib/pangea/models/constructs_model.dart b/lib/pangea/models/constructs_model.dart deleted file mode 100644 index 2bdf00a81..000000000 --- a/lib/pangea/models/constructs_model.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/models/analytics_model.dart'; -import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; -import 'package:flutter/material.dart'; - -import '../enum/construct_type_enum.dart'; - -class ConstructAnalyticsModel extends AnalyticsModel { - ConstructType type; - List uses; - - ConstructAnalyticsModel({ - required this.type, - this.uses = const [], - super.lastUpdated, - super.prevEventId, - super.prevLastUpdated, - }); - - static const _lastUpdatedKey = "lupt"; - static const _usesKey = "uses"; - - factory ConstructAnalyticsModel.fromJson(Map json) { - // try { - // debugger( - // when: - // kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null), - // ); - return ConstructAnalyticsModel( - // lemma: json[ModelKey.lemma], - // uses: (json['uses'] as Iterable) - // .map( - // (use) => use != null ? OneConstructUse.fromJson(use) : null, - // ) - // .where((element) => element != null) - // .cast() - // .toList(), - type: ConstructTypeUtil.fromString(json['type']), - lastUpdated: json[_lastUpdatedKey] != null - ? DateTime.parse(json[_lastUpdatedKey]) - : null, - prevEventId: json[ModelKey.prevEventId], - prevLastUpdated: json[ModelKey.prevLastUpdated] != null - ? DateTime.parse(json[ModelKey.prevLastUpdated]) - : null, - uses: json[_usesKey] - .values - .map((lemmaUses) => LemmaConstructsModel.fromJson(lemmaUses)) - .cast() - .toList(), - ); - // } catch (err) { - // debugger(when: kDebugMode); - // rethrow; - // } - } - - toJson() { - final Map usesMap = {}; - for (final use in uses) { - usesMap[use.lemma] = use.toJson(); - } - - return { - // ModelKey.lemma: lemma, - // 'uses': uses.map((use) => use.toJson()).toList(), - 'type': type.string, - _lastUpdatedKey: lastUpdated?.toIso8601String(), - _usesKey: usesMap, - ModelKey.prevEventId: prevEventId, - ModelKey.prevLastUpdated: prevLastUpdated?.toIso8601String(), - }; - } - - // void addUsesByUseType(List uses) { - // for (final use in uses) { - // if (use.lemma != lemma) { - // throw Exception('lemma mismatch'); - // } - // uses.add(use); - // } - // } -} - -class LemmaConstructsModel { - String lemma; - List uses; - - LemmaConstructsModel({ - required this.lemma, - this.uses = const [], - }); - - factory LemmaConstructsModel.fromJson(Map json) { - return LemmaConstructsModel( - lemma: json[ModelKey.lemma], - uses: (json['uses'] ?? [] as Iterable) - .map( - (use) => use != null ? OneConstructUse.fromJson(use) : null, - ) - .where((element) => element != null) - .cast() - .toList(), - ); - } - - Map toJson() { - return { - ModelKey.lemma: lemma, - 'uses': uses.map((use) => use.toJson()).toList(), - }; - } -} - -enum ConstructUseType { - /// produced in chat by user, igc was run, and we've judged it to be a correct use - wa, - - /// produced in chat by user, igc was run, and we've judged it to be a incorrect use - /// Note: if the IGC match is ignored, this is not counted as an incorrect use - ga, - - /// produced in chat by user and igc was not run - unk, - - /// selected correctly in IT flow - corIt, - - /// encountered as IT distractor and correctly ignored it - ignIt, - - /// encountered as it distractor and selected it - incIt, - - /// encountered in igc match and ignored match - ignIGC, - - /// selected correctly in IGC flow - corIGC, - - /// encountered as distractor in IGC flow and selected it - incIGC, -} - -extension on ConstructUseType { - String get string { - switch (this) { - case ConstructUseType.ga: - return 'ga'; - case ConstructUseType.wa: - return 'wa'; - case ConstructUseType.corIt: - return 'corIt'; - case ConstructUseType.incIt: - return 'incIt'; - case ConstructUseType.ignIt: - return 'ignIt'; - case ConstructUseType.ignIGC: - return 'ignIGC'; - case ConstructUseType.corIGC: - return 'corIGC'; - case ConstructUseType.incIGC: - return 'incIGC'; - case ConstructUseType.unk: - return 'unk'; - } - } - - IconData get icon { - switch (this) { - case ConstructUseType.ga: - return Icons.check; - case ConstructUseType.wa: - return Icons.thumb_up_sharp; - case ConstructUseType.corIt: - return Icons.check; - case ConstructUseType.incIt: - return Icons.close; - case ConstructUseType.ignIt: - return Icons.close; - case ConstructUseType.ignIGC: - return Icons.close; - case ConstructUseType.corIGC: - return Icons.check; - case ConstructUseType.incIGC: - return Icons.close; - case ConstructUseType.unk: - return Icons.help; - } - } -} diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 3586253b8..497381fa1 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/lib/pangea/models/student_analytics_event.dart b/lib/pangea/models/student_analytics_event.dart deleted file mode 100644 index 77279eb8d..000000000 --- a/lib/pangea/models/student_analytics_event.dart +++ /dev/null @@ -1,165 +0,0 @@ -// import 'dart:developer'; - -// import 'package:fluffychat/pangea/extensions/client_extension.dart'; -// import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; -// import 'package:fluffychat/pangea/utils/error_handler.dart'; -// import 'package:flutter/foundation.dart'; -// import 'package:matrix/matrix.dart'; - - -// import '../constants/pangea_event_types.dart'; -// import 'chart_analytics_model.dart'; - -// class StudentAnalyticsEvent { -// late Event _event; -// StudentAnalyticsSummary? _contentCache; -// List _messagesToSave = []; - -// StudentAnalyticsEvent({required Event event}) { -// if (event.type != PangeaEventTypes.studentAnalyticsSummary) { -// throw Exception( -// "${event.type} should not be used to make a StudentAnalyticsEvent", -// ); -// } -// _event = event; -// _messagesToSave = []; -// } - -// Event get event => _event; - -// StudentAnalyticsSummary get content { -// _contentCache ??= StudentAnalyticsSummary.fromJson(event.content); -// return _contentCache!; -// } - -// Future removeEdittedMessages( -// RecentMessageRecord message, -// ) async { -// final List removeIds = await event.room.client.getEditHistory( -// message.chatId, -// message.eventId, -// ); -// if (removeIds.isEmpty) return; -// _messagesToSave.removeWhere( -// (msg) => removeIds.any((e) => e == msg.eventId), -// ); -// content.removeEdittedMessages( -// event.room.client, -// removeIds, -// ); -// } - -// // Future handleNewMessage( -// // RecentMessageRecord message, { -// // isEdit = false, -// // }) async { -// // if (isEdit) { -// // await removeEdittedMessages(message); -// // } -// // // _addMessage(message); -// // _messagesToSave.add(message); -// // debugPrint("messages to save is now: ${_messagesToSave.length}"); - -// // if (DateTime.now().difference(content.lastUpdated).inMinutes > -// // ClassDefaultValues.minutesDelayToUpdateMyAnalytics) { -// // _updateStudentAnalytics(); -// // } -// // } - -// // Future bulkUpdate(List messages) async { -// // // if (event.room.client.userID != _event.stateKey) { -// // // debugger(when: kDebugMode); -// // // ErrorHandler.logError( -// // // m: "should not be in bulkUpdate ${event.room.client.userID} != ${_event.stateKey}", -// // // ); -// // // return; -// // // } -// // for (final message in messages) { -// // await removeEdittedMessages(message); -// // } - -// // _messagesToSave.addAll(messages); -// // await _updateStudentAnalytics(); -// // } - -// // Future _updateStudentAnalytics() async { -// // content.lastUpdated = DateTime.now(); -// // content.addAll(_messagesToSave); -// // _clearMessages(); - -// // await event.room.client.setRoomStateWithKey( -// // event.room.id, -// // _event.type, -// // '', -// // content.toJson(), -// // ); -// // } - -// Future updateStudentAnalytics() async { -// content.lastUpdated = DateTime.now(); -// // content.addAll(_messagesToSave); -// // _clearMessages(); - -// await event.room.client.setRoomStateWithKey( -// event.room.id, -// _event.type, -// '', -// content.toJson(), -// ); -// await event.room.postLoad(); -// } - -// _addMessage(RecentMessageRecord message) { -// if (_messagesToSave.every((e) => e.eventId != message.eventId)) { -// _messagesToSave.add(message); -// } else { -// debugger(when: kDebugMode); -// ErrorHandler.logError( -// m: "adding message twice in StudentAnalyticsEvent._addMessage", -// ); -// } -// //PTODO - save to local storagge -// } - -// _clearMessages() { -// _messagesToSave.clear(); -// //PTODO - clear local storagge -// } - -// Future getTotals(String? chatId) async { -// final TimeSeriesTotals totals = TimeSeriesTotals.empty; -// final msgs = chatId == null -// ? content.messages -// : content.messages.where((msg) => msg.chatId == chatId); -// for (final msg in msgs) { -// totals.increment(msg); -// } -// return totals; -// } - -// Future getTimeServiesInterval( -// DateTime start, -// DateTime end, -// String? chatId, -// ) async { -// final TimeSeriesInterval interval = TimeSeriesInterval( -// start: start, -// end: end, -// totals: TimeSeriesTotals.empty, -// ); -// for (final msg in content.messages) { -// if (msg.time.isAfter(start) && -// msg.time.isBefore(end) && -// (chatId == null || chatId == msg.chatId)) { -// interval.totals.increment(msg); -// } -// } -// return interval; -// } - -// bool isAlreadyAdded(RecentMessageRecord message) { -// return content.messages.any( -// (element) => element.eventId == message.eventId, -// ); -// } -// } diff --git a/lib/pangea/models/student_analytics_event_old.dart b/lib/pangea/models/student_analytics_event_old.dart deleted file mode 100644 index d2696eb01..000000000 --- a/lib/pangea/models/student_analytics_event_old.dart +++ /dev/null @@ -1,51 +0,0 @@ -// import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -// import 'package:fluffychat/pangea/models/analytics_model_older.dart'; -// import 'package:matrix/matrix.dart'; - -// import '../constants/pangea_event_types.dart'; - -// class StudentAnalyticsEvent { -// late Event _event; -// StudentAnalyticsSummary? _contentCache; - -// StudentAnalyticsEvent({required Event event}) { -// if (event.type != PangeaEventTypes.studentAnalyticsSummary) { -// throw Exception( -// "${event.type} should not be used to make a StudentAnalyticsEvent", -// ); -// } -// _event = event; -// } - -// Event get event => _event; - -// StudentAnalyticsSummary get _content { -// _contentCache ??= event.getPangeaContent(); -// return _contentCache!; -// } - -// List get monthly => _content.monthlyTotalsForAllTime; -// List get daily => _content.dailyTotalsForLast30Days; -// List get hourly => _content.hourlyTotalsForLast24Hours; - -// // updateLocal -// // updateServer -// handleNewMessage() {} - -// /// if monthly.isNotEmpty && last.end.month < now.month -// /// push empty intervals until last.end.month >= now.month -// /// if daily.isEmpty -// /// push empty intervals until last.end.day >= now.day -// /// else if daily.where(e => e.month < now.month) -// /// sum and add to monthly -// /// -// /// if hourly.isEmpty || last.end.hour < now.hour -// /// push empty intervals until last.end.hour >= now.hour -// /// increment hourly - -// updateLocal() {} - -// // if server copy is older than x, push local version -// // get new server copy, local version = server copy -// updateServer() {} -// } diff --git a/lib/pangea/models/summary_analytics_event.dart b/lib/pangea/models/summary_analytics_event.dart deleted file mode 100644 index 1d447a854..000000000 --- a/lib/pangea/models/summary_analytics_event.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:fluffychat/pangea/models/analytics_event.dart'; -import 'package:fluffychat/pangea/models/summary_analytics_model.dart'; -import 'package:matrix/matrix.dart'; - -import '../constants/pangea_event_types.dart'; - -class SummaryAnalyticsEvent extends AnalyticsEvent { - SummaryAnalyticsEvent({required Event event}) : super(event: event) { - if (event.type != PangeaEventTypes.summaryAnalytics) { - throw Exception( - "${event.type} should not be used to make a SummaryAnalyticsEvent", - ); - } - } - - @override - SummaryAnalyticsModel get content { - contentCache ??= SummaryAnalyticsModel.fromJson(event.content); - return contentCache as SummaryAnalyticsModel; - } -} diff --git a/lib/pangea/models/summary_analytics_model.dart b/lib/pangea/models/summary_analytics_model.dart deleted file mode 100644 index 19ef857a9..000000000 --- a/lib/pangea/models/summary_analytics_model.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:convert'; - -import 'package:fluffychat/pangea/constants/model_keys.dart'; -import 'package:fluffychat/pangea/models/analytics_model.dart'; -import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:flutter/foundation.dart'; - -class SummaryAnalyticsModel extends AnalyticsModel { - late List _messages; - - SummaryAnalyticsModel({ - required List messages, - super.lastUpdated, - super.prevEventId, - super.prevLastUpdated, - }) { - _messages = messages; - } - - List get messages => _messages; - - static const _messagesKey = "msgs"; - static const _lastUpdatedKey = "lupt"; - - Map toJson() => { - _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), - _lastUpdatedKey: lastUpdated?.toIso8601String(), - ModelKey.prevEventId: prevEventId, - ModelKey.prevLastUpdated: prevLastUpdated?.toIso8601String(), - }; - - factory SummaryAnalyticsModel.fromJson(json) { - List savedMessages = []; - try { - savedMessages = json[_messagesKey] != null - ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) - .map((e) => RecentMessageRecord.fromJson(e)) - .toList() - .cast() - : []; - } catch (err, stack) { - if (kDebugMode) rethrow; - ErrorHandler.logError(e: err, s: stack); - } - return SummaryAnalyticsModel( - messages: savedMessages, - lastUpdated: json[_lastUpdatedKey] != null - ? DateTime.parse(json[_lastUpdatedKey]) - : null, - prevEventId: json[ModelKey.prevEventId], - prevLastUpdated: json[ModelKey.prevLastUpdated] != null - ? DateTime.parse(json[ModelKey.prevLastUpdated]) - : null, - ); - } -} diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index b5a2bd770..991a565c8 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -11,7 +11,7 @@ import 'package:matrix/matrix.dart'; import '../../../../utils/date_time_extension.dart'; import '../../../widgets/avatar.dart'; import '../../../widgets/matrix.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; import 'base_analytics.dart'; import 'list_summary_analytics.dart'; diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 7829407ed..fde016ee2 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -4,7 +4,7 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.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'; -import 'package:fluffychat/pangea/models/analytics_event.dart'; +import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; @@ -14,7 +14,7 @@ import '../../../widgets/matrix.dart'; import '../../controllers/pangea_controller.dart'; import '../../enum/bar_chart_view_enum.dart'; import '../../enum/time_span.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; class BaseAnalyticsPage extends StatefulWidget { final String pageTitle; @@ -73,10 +73,14 @@ class BaseAnalyticsController extends State { final List analyticsEvent = []; for (final analyticsRoom in analyticsRooms) { - final lastSummaryEvent = await analyticsRoom - .getLastAnalyticsEvent(PangeaEventTypes.summaryAnalytics); - final lastConstructEvent = - await analyticsRoom.getLastAnalyticsEvent(PangeaEventTypes.construct); + final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent( + PangeaEventTypes.summaryAnalytics, + Matrix.of(context).client.userID!, + ); + final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent( + PangeaEventTypes.construct, + Matrix.of(context).client.userID!, + ); if (lastSummaryEvent != null) { analyticsEvent.add(lastSummaryEvent); } diff --git a/lib/pangea/pages/analytics/class_list/class_list.dart b/lib/pangea/pages/analytics/class_list/class_list.dart index 6b0790863..4fb9ed765 100644 --- a/lib/pangea/pages/analytics/class_list/class_list.dart +++ b/lib/pangea/pages/analytics/class_list/class_list.dart @@ -8,7 +8,7 @@ import 'package:matrix/matrix.dart'; import '../../../../widgets/matrix.dart'; import '../../../controllers/pangea_controller.dart'; -import '../../../models/chart_analytics_model.dart'; +import '../../../models/analytics/chart_analytics_model.dart'; import '../../../utils/sync_status_util_v2.dart'; import '../../../widgets/common/list_placeholder.dart'; diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index 2a77abc34..d555b4378 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -2,12 +2,11 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.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/constructs_analytics_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'; @@ -223,7 +222,7 @@ class ConstructListViewState extends State { if (widget.pangeaController.analytics.constructs == null) { return null; } - return widget.pangeaController.myAnalytics + return widget.pangeaController.analytics .aggregateConstructData(widget.pangeaController.analytics.constructs!) .where((lemmaUses) => lemmaUses.uses.isNotEmpty) .sorted((a, b) { diff --git a/lib/pangea/pages/analytics/list_summary_analytics.dart b/lib/pangea/pages/analytics/list_summary_analytics.dart index 5b2dde5de..bf388cea7 100644 --- a/lib/pangea/pages/analytics/list_summary_analytics.dart +++ b/lib/pangea/pages/analytics/list_summary_analytics.dart @@ -1,10 +1,9 @@ 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 'package:fluffychat/pangea/models/chart_analytics_model.dart'; import '../../enum/use_type.dart'; class ListSummaryAnalytics extends StatelessWidget { diff --git a/lib/pangea/pages/analytics/messages_bar_chart.dart b/lib/pangea/pages/analytics/messages_bar_chart.dart index d90e44fe4..509270edb 100644 --- a/lib/pangea/pages/analytics/messages_bar_chart.dart +++ b/lib/pangea/pages/analytics/messages_bar_chart.dart @@ -10,7 +10,7 @@ import 'package:intl/intl.dart'; import '../../enum/time_span.dart'; import '../../enum/use_type.dart'; -import '../../models/chart_analytics_model.dart'; +import '../../models/analytics/chart_analytics_model.dart'; import 'bar_chart_card.dart'; import 'messages_legend_widget.dart'; @@ -58,10 +58,10 @@ class MessagesBarChartState extends State { getTitlesWidget: leftTitles, ), ), - topTitles: AxisTitles( + topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), - rightTitles: AxisTitles( + rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), );