updates to how analytics events are processed, stored, and displayed. Added automatic updating of student analytics events.
This commit is contained in:
parent
0e47b84552
commit
20cdc3796a
35 changed files with 989 additions and 1568 deletions
|
|
@ -845,6 +845,7 @@ class ChatListController extends State<ChatList>
|
|||
if (mounted) {
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
await pangeaController.subscriptionController.initialize();
|
||||
await pangeaController.myAnalytics.addEventsListener();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<SummaryAnalyticsEvent>> allMySummaryAnalytics() async {
|
||||
Future<DateTime?> 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<Room> analyticsRooms = _pangeaController
|
||||
.matrixState.client.allMyAnalyticsRooms
|
||||
.where((room) => room.isAnalyticsRoom)
|
||||
.toList();
|
||||
|
||||
final List<DateTime> 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<DateTime?> 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<Future<DateTime?>> lastUpdatedFutures = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom == null) continue;
|
||||
lastUpdatedFutures.add(
|
||||
analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
student.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<DateTime?> lastUpdatedWithNulls =
|
||||
await Future.wait(lastUpdatedFutures);
|
||||
final List<DateTime> lastUpdates =
|
||||
lastUpdatedWithNulls.where((e) => e != null).cast<DateTime>().toList();
|
||||
if (lastUpdates.isNotEmpty) {
|
||||
return lastUpdates.reduce(
|
||||
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> 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<SummaryAnalyticsEvent> allEvents = [];
|
||||
|
||||
// TODO switch to using list of futures
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<SummaryAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
))
|
||||
?.cast<SummaryAnalyticsEvent>();
|
||||
allEvents.addAll(roomEvents ?? []);
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
|
||||
allEvents.addAll(
|
||||
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
|
||||
);
|
||||
}
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> allSpaceMemberAnalytics(
|
||||
Future<List<SummaryAnalyticsEvent>> 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<SummaryAnalyticsEvent> 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<SummaryAnalyticsEvent> analyticsEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
|
||||
if (analyticsRoom != null) {
|
||||
final List<SummaryAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
))
|
||||
?.cast<SummaryAnalyticsEvent>();
|
||||
analyticsEvents.addAll(roomEvents ?? []);
|
||||
userId: student.id,
|
||||
);
|
||||
analyticsEvents.addAll(
|
||||
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// get a list of all the space's children, including sub-space children
|
||||
final resp = await space.client.getSpaceHierarchy(space.id);
|
||||
final List<String> spaceChildrenIds =
|
||||
resp.rooms.map((room) => room.roomId).toList();
|
||||
|
||||
// filter out the analyics events that don't belong to the space's children
|
||||
final List<SummaryAnalyticsEvent> 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<SummaryAnalyticsEvent> 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<SummaryAnalyticsEvent> 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<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
allConstructs.addAll(roomEvents ?? []);
|
||||
|
|
@ -430,6 +565,8 @@ class AnalyticsController extends BaseController {
|
|||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: student.id,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
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<ConstructAnalyticsEvent> 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<List<ConstructAnalyticsEvent>> 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<ConstructAnalyticsEvent>? 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<AggregateConstructUses> aggregateConstructData(
|
||||
List<ConstructAnalyticsEvent> constructs,
|
||||
) {
|
||||
final Map<String, List<LemmaConstructsModel>> lemmasToConstructs = {};
|
||||
for (final construct in constructs) {
|
||||
for (final lemmaUses in construct.content.uses) {
|
||||
lemmasToConstructs[lemmaUses.lemma] ??= [];
|
||||
lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses);
|
||||
}
|
||||
}
|
||||
|
||||
final List<AggregateConstructUses> aggregatedConstructs = [];
|
||||
for (final lemmaToConstructs in lemmasToConstructs.entries) {
|
||||
final List<LemmaConstructsModel> 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<ConstructAnalyticsEvent> 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<ConstructAnalyticsEvent> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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<void> addEventsListener() async {
|
||||
final Client client = _pangeaController.matrixState.client;
|
||||
|
||||
Future<void> 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<Event>? 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<String> 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<String> 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<String>;
|
||||
} catch (err) {
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
local: true,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<Room> 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<void> 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<String, AnalyticsEvent?> 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<DateTime?> lastUpdates = prevEvents.values
|
||||
.map((e) => e?.content.lastUpdated)
|
||||
.cast<DateTime?>()
|
||||
.toList();
|
||||
|
||||
DateTime? earliestLastUpdated;
|
||||
if (!lastUpdates.any((updated) => updated == null)) {
|
||||
earliestLastUpdated = lastUpdates.reduce(
|
||||
(min, e) => e!.isBefore(min!) ? e : min,
|
||||
);
|
||||
}
|
||||
|
||||
final List<RecentMessageRecord> analyticsContent = [];
|
||||
final List<OneConstructUse> 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<dynamic> 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<PangeaMessageEvent> 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<String, List<OneConstructUse>> 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<PangeaMessageEvent>? 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<RecentMessageRecord> formatAnalyticsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
SummaryAnalyticsEvent? prevEvent,
|
||||
) {
|
||||
List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
if (prevEvent?.content.lastUpdated != null) {
|
||||
filtered = recentMsgs
|
||||
.where(
|
||||
(msg) => msg.event.originServerTs.isAfter(
|
||||
prevEvent!.content.lastUpdated!,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final List<String> addedMsgIds =
|
||||
prevEvent?.content.messages.map((msg) => msg.eventId).toList() ?? [];
|
||||
|
||||
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
|
||||
|
||||
final List<RecentMessageRecord> 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<List<Room>> createMissingAnalyticsRooms() async {
|
||||
List<String> targetLangs = [];
|
||||
final String? userL2 = _pangeaController.languageController.activeL2Code();
|
||||
if (userL2 != null) targetLangs.add(userL2);
|
||||
final List<String?> 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<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
ConstructAnalyticsEvent? prevEvent,
|
||||
) {
|
||||
List<PangeaMessageEvent> 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<String>());
|
||||
targetLangs = targetLangs.toSet().toList();
|
||||
for (final String langCode in targetLangs) {
|
||||
await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
}
|
||||
|
||||
final List<String> addedMsgIds = prevEvent?.content.uses
|
||||
.map((lemmause) => lemmause.uses.map((use) => use.msgId))
|
||||
.expand((element) => element)
|
||||
.where((element) => element != null)
|
||||
.cast<String>()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
|
||||
|
||||
final List<OneConstructUse> uses = filtered
|
||||
.map(
|
||||
(msg) => msg.originalSent?.choreo?.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<List<OneConstructUse>>()
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
|
||||
return uses;
|
||||
return _pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
}
|
||||
|
||||
List<Room> _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<List<Room>> createMissingAnalyticsRoom() async {
|
||||
List<String> targetLangs = [];
|
||||
final String? userL2 = _pangeaController.languageController.activeL2Code();
|
||||
if (userL2 != null) targetLangs.add(userL2);
|
||||
final List<String?> spaceL2s = studentSpaces
|
||||
.map(
|
||||
(space) => _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast<String>());
|
||||
targetLangs = targetLangs.toSet().toList();
|
||||
for (final String langCode in targetLangs) {
|
||||
await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
}
|
||||
return _pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
}
|
||||
|
||||
Future<void> updateAnalytics() async {
|
||||
await setStudentChats();
|
||||
await setStudentSpaces();
|
||||
final List<Room> 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<AggregateConstructUses> aggregateConstructData(
|
||||
List<ConstructAnalyticsEvent> constructs,
|
||||
) {
|
||||
final Map<String, List<LemmaConstructsModel>> lemmasToConstructs = {};
|
||||
for (final construct in constructs) {
|
||||
for (final lemmaUses in construct.content.uses) {
|
||||
lemmasToConstructs[lemmaUses.lemma] ??= [];
|
||||
lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses);
|
||||
}
|
||||
}
|
||||
|
||||
final List<AggregateConstructUses> aggregatedConstructs = [];
|
||||
for (final lemmaToConstructs in lemmasToConstructs.entries) {
|
||||
final List<LemmaConstructsModel> lemmaConstructs =
|
||||
lemmaToConstructs.value;
|
||||
final AggregateConstructUses aggregatedData = AggregateConstructUses(
|
||||
lemmaUses: lemmaConstructs,
|
||||
);
|
||||
aggregatedConstructs.add(aggregatedData);
|
||||
}
|
||||
return aggregatedConstructs;
|
||||
}
|
||||
|
||||
Future<DateTime?> analyticsLastUpdated(String type) async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
if (analyticsRooms.isEmpty) return null;
|
||||
final List<DateTime> 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<LemmaConstructsModel> _lemmaUses;
|
||||
|
||||
AggregateConstructUses({required List<LemmaConstructsModel> lemmaUses})
|
||||
: _lemmaUses = lemmaUses;
|
||||
|
||||
String get lemma {
|
||||
assert(
|
||||
_lemmaUses.isNotEmpty &&
|
||||
_lemmaUses.every(
|
||||
(construct) => construct.lemma == _lemmaUses.first.lemma,
|
||||
),
|
||||
);
|
||||
return _lemmaUses.first.lemma;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get uses => _lemmaUses
|
||||
.map((lemmaUse) => lemmaUse.uses)
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<PangeaMessageEvent>> myMessageEventsInChat({
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final List<Event> 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<List<Event>> 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<Event> 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<PangeaMessageEvent> 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<Event> 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<Event> 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);
|
||||
|
|
|
|||
|
|
@ -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<AnalyticsEvent?> getLastAnalyticsEvent(
|
||||
String type,
|
||||
String userId,
|
||||
) async =>
|
||||
await _getLastAnalyticsEvent(type);
|
||||
await _getLastAnalyticsEvent(type, userId);
|
||||
|
||||
Future<AnalyticsEvent?> getPrevAnalyticsEvent(
|
||||
AnalyticsEvent analyticsEvent,
|
||||
) async =>
|
||||
await _getPrevAnalyticsEvent(analyticsEvent);
|
||||
Future<DateTime?> analyticsLastUpdated(String type, String userId) async {
|
||||
return await _analyticsLastUpdated(type, userId);
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -197,77 +197,53 @@ extension AnalyticsRoomExtension on Room {
|
|||
|
||||
Future<AnalyticsEvent?> _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<Event> 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<AnalyticsEvent?> _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<DateTime?> _analyticsLastUpdated(String type, String userId) async {
|
||||
final lastEvent = await _getLastAnalyticsEvent(type, userId);
|
||||
return lastEvent?.event.originServerTs;
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> _getAnalyticsEvents({
|
||||
required String type,
|
||||
required String userId,
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final AnalyticsEvent? mostRecentEvent = await getLastAnalyticsEvent(type);
|
||||
if (mostRecentEvent == null) return null;
|
||||
final List<AnalyticsEvent> 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<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
sender: userId,
|
||||
since: since,
|
||||
);
|
||||
final List<AnalyticsEvent> 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 {
|
||||
|
|
|
|||
59
lib/pangea/models/analytics/analytics_event.dart
Normal file
59
lib/pangea/models/analytics/analytics_event.dart
Normal file
|
|
@ -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<String> analyticsEventTypes = [
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
PangeaEventTypes.construct,
|
||||
];
|
||||
|
||||
static Future<String?> sendEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
List<dynamic> analyticsContent,
|
||||
) async {
|
||||
String? eventId;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<RecentMessageRecord>(),
|
||||
);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
eventId = await ConstructAnalyticsEvent.sendConstructsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<OneConstructUse>(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
19
lib/pangea/models/analytics/analytics_model.dart
Normal file
19
lib/pangea/models/analytics/analytics_model.dart
Normal file
|
|
@ -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<dynamic> formatAnalyticsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
String type,
|
||||
) {
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
65
lib/pangea/models/analytics/constructs_event.dart
Normal file
65
lib/pangea/models/analytics/constructs_event.dart
Normal file
|
|
@ -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<String?> sendConstructsEvent(
|
||||
Room analyticsRoom,
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
// create a map of lemmas to their uses
|
||||
final Map<String, List<OneConstructUse>> 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<LemmaConstructsModel> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<LemmaConstructsModel> uses;
|
||||
|
||||
List<OneConstructUse> 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<String, dynamic> json) {
|
||||
// try {
|
||||
debugger(
|
||||
when:
|
||||
kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null),
|
||||
static const _usesKey = "uses";
|
||||
|
||||
factory ConstructAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
return ConstructAnalyticsModel(
|
||||
type: ConstructTypeUtil.fromString(json['type']),
|
||||
uses: json[_usesKey]
|
||||
.values
|
||||
.map((lemmaUses) => LemmaConstructsModel.fromJson(lemmaUses))
|
||||
.cast<LemmaConstructsModel>()
|
||||
.toList(),
|
||||
);
|
||||
return ConstructUses(
|
||||
}
|
||||
|
||||
toJson() {
|
||||
final Map<String, dynamic> usesMap = {};
|
||||
for (final use in uses) {
|
||||
usesMap[use.lemma] = use.toJson();
|
||||
}
|
||||
|
||||
return {
|
||||
'type': type.string,
|
||||
_usesKey: usesMap,
|
||||
};
|
||||
}
|
||||
|
||||
static List<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<OneConstructUse> uses = filtered
|
||||
.map(
|
||||
(msg) => msg.originalSent?.choreo?.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<List<OneConstructUse>>()
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
class LemmaConstructsModel {
|
||||
String lemma;
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
LemmaConstructsModel({
|
||||
required this.lemma,
|
||||
this.uses = const [],
|
||||
});
|
||||
|
||||
factory LemmaConstructsModel.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaConstructsModel(
|
||||
lemma: json[ModelKey.lemma],
|
||||
uses: (json['uses'] as Iterable)
|
||||
uses: (json['uses'] ?? [] as Iterable)
|
||||
.map<OneConstructUse?>(
|
||||
(use) => use != null ? OneConstructUse.fromJson(use) : null,
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
type: ConstructTypeUtil.fromString(json['type']),
|
||||
);
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
toJson() {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
ModelKey.lemma: lemma,
|
||||
'uses': uses.map((use) => use.toJson()).toList(),
|
||||
'type': type.string,
|
||||
};
|
||||
}
|
||||
|
||||
void addUsesByUseType(List<OneConstructUse> 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<LemmaConstructsModel> _lemmaUses;
|
||||
|
||||
AggregateConstructUses({required List<LemmaConstructsModel> lemmaUses})
|
||||
: _lemmaUses = lemmaUses;
|
||||
|
||||
String get lemma {
|
||||
assert(
|
||||
_lemmaUses.isNotEmpty &&
|
||||
_lemmaUses.every(
|
||||
(construct) => construct.lemma == _lemmaUses.first.lemma,
|
||||
),
|
||||
);
|
||||
return _lemmaUses.first.lemma;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get uses => _lemmaUses
|
||||
.map((lemmaUse) => lemmaUse.uses)
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
}
|
||||
35
lib/pangea/models/analytics/summary_analytics_event.dart
Normal file
35
lib/pangea/models/analytics/summary_analytics_event.dart
Normal file
|
|
@ -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<String?> sendSummaryAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RecentMessageRecord> _messages;
|
||||
|
||||
SummaryAnalyticsModel({
|
||||
required List<RecentMessageRecord> messages,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
};
|
||||
|
||||
factory SummaryAnalyticsModel.fromJson(json) {
|
||||
List<RecentMessageRecord> savedMessages = [];
|
||||
try {
|
||||
savedMessages = json[_messagesKey] != null
|
||||
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
|
||||
.map((e) => RecentMessageRecord.fromJson(e))
|
||||
.toList()
|
||||
.cast<RecentMessageRecord>()
|
||||
: [];
|
||||
} catch (err, stack) {
|
||||
if (kDebugMode) rethrow;
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
return SummaryAnalyticsModel(
|
||||
messages: savedMessages,
|
||||
);
|
||||
}
|
||||
|
||||
static List<RecentMessageRecord> formatSummaryContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<RecentMessageRecord> 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<RecentMessageRecord> _messages;
|
||||
DateTime? lastUpdated;
|
||||
|
||||
StudentAnalyticsSummary({
|
||||
required List<RecentMessageRecord> messages,
|
||||
required this.lastUpdated,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
void addAll(List<RecentMessageRecord> msgs) {
|
||||
for (final msg in msgs) {
|
||||
if (!(_messages.any((element) => element.eventId == msg.eventId))) {
|
||||
_messages.add(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeEdittedMessages(Client client, List<String> removeEventIds) {
|
||||
_messages.removeWhere(
|
||||
(element) => removeEventIds.contains(element.eventId),
|
||||
);
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
static const _lastUpdatedKey = "lupt";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
_lastUpdatedKey: lastUpdated?.toIso8601String(),
|
||||
};
|
||||
|
||||
factory StudentAnalyticsSummary.fromJson(json) {
|
||||
List<RecentMessageRecord> savedMessages = [];
|
||||
try {
|
||||
savedMessages = json[_messagesKey] != null
|
||||
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
|
||||
.map((e) => RecentMessageRecord.fromJson(e))
|
||||
.toList()
|
||||
.cast<RecentMessageRecord>()
|
||||
: [];
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
abstract class AnalyticsModel {
|
||||
DateTime? lastUpdated;
|
||||
String? prevEventId;
|
||||
DateTime? prevLastUpdated;
|
||||
|
||||
AnalyticsModel({
|
||||
this.lastUpdated,
|
||||
this.prevEventId,
|
||||
this.prevLastUpdated,
|
||||
});
|
||||
}
|
||||
|
|
@ -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<String, dynamic> 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<UserTimeSeriesInterval> users;
|
||||
|
||||
// TimeSeriesInterval({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.users,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> 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<UserTimeSeriesInterval>()),
|
||||
// );
|
||||
// }
|
||||
|
||||
// class RoomAnalyticsSummary {
|
||||
// List<TimeSeriesInterval> monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> hourlyTotalsForLast24Hours;
|
||||
|
||||
// DateTime? updatedAt;
|
||||
|
||||
// RoomAnalyticsSummary({
|
||||
// required this.monthlyTotalsForAllTime,
|
||||
// required this.dailyTotalsForLast30Days,
|
||||
// required this.hourlyTotalsForLast24Hours,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> 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<TimeSeriesInterval>(),
|
||||
// dailyTotalsForLast30Days: (jsonDecode(json["dys"]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// hourlyTotalsForLast24Hours: (jsonDecode(json["hrs"]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// class UserDirectChatAnalyticsSummary {
|
||||
// // directChatRoomIds and analytics for those rooms
|
||||
// // updated by user;
|
||||
// Map<String, RoomAnalyticsSummary>? directChatSummaries;
|
||||
|
||||
// Map<String, dynamic> toJson() => {};
|
||||
// }
|
||||
|
||||
// // maybe search how to do date ranges in dart
|
||||
|
|
@ -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<String, dynamic> 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<ChatTimeSeriesInterval> chats;
|
||||
|
||||
// TimeSeriesInterval({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.chats,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> 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<ChatTimeSeriesInterval>()),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // class RecentMessageRecord {
|
||||
// // String eventId;
|
||||
// // String typeOfUse;
|
||||
// // String time;
|
||||
// // }
|
||||
|
||||
// class StudentAnalyticsSummary {
|
||||
// /// event statekey = studentId
|
||||
// // String studentId;
|
||||
|
||||
// List<TimeSeriesInterval> monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> hourlyTotalsForLast24Hours;
|
||||
|
||||
// // List<RecentMessageRecord> 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<String, dynamic> 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<TimeSeriesInterval>(),
|
||||
// dailyTotalsForLast30Days: (jsonDecode(json[_dayKey]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// hourlyTotalsForLast24Hours: (jsonDecode(json[_hoursKey]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// lastLogin: DateTime(json[_lastLoginKey]),
|
||||
// lastUpdated: DateTime(json[_lastLoginKey]),
|
||||
// lastMessage: DateTime(json[_lastMessageKey]),
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// import 'dart:convert';
|
||||
|
||||
// class BaseDataModel {
|
||||
// late int spanTotal;
|
||||
// late int spanIT;
|
||||
// late int spanIGC;
|
||||
// late int spanDirect;
|
||||
|
||||
// BaseDataModel(Map<String, dynamic> json) {
|
||||
// fromJson(json);
|
||||
// }
|
||||
|
||||
// fromJson(Map<String, dynamic> 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<String, dynamic> json) : super(json) {
|
||||
// fromJsonTimeSeriesInterval(json);
|
||||
// }
|
||||
|
||||
// fromJsonTimeSeriesInterval(Map<String, dynamic> 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<String>? chatIds;
|
||||
// late List<String>? userIds;
|
||||
// late List<String>? classIds;
|
||||
// late List<TimeSeriesInterval> timeSeries;
|
||||
|
||||
// chartAnalytics(Map<String, dynamic> json) : super(json) {
|
||||
// fromJsonchartAnalytics(json);
|
||||
// fetchedAt = DateTime.now();
|
||||
// }
|
||||
|
||||
// fromJsonchartAnalytics(Map<String, dynamic> 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<dynamic>)
|
||||
// .map(
|
||||
// (timeSeriesJsonEntry) => TimeSeriesInterval(timeSeriesJsonEntry),
|
||||
// )
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>();
|
||||
// chatIds = json["chats"] != null && json["chats"] != []
|
||||
// ? (json["chats"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// userIds = json["users"] != null && json["userIds"] != []
|
||||
// ? (json["users"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// classIds = json["classes"] != null && json["classes"] != []
|
||||
// ? (json["classes"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
class ClassAnalyticsModel {
|
||||
ClassAnalyticsModel();
|
||||
late final Null classId;
|
||||
late final List<String> userIds;
|
||||
late final List<Analytics> analytics;
|
||||
get tableView {}
|
||||
ClassAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
classId = null;
|
||||
userIds = List.castFrom<dynamic, String>(json['user_ids']);
|
||||
analytics =
|
||||
List.from(json['analytics']).map((e) => Analytics.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
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> section;
|
||||
|
||||
Analytics.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
section =
|
||||
List.from(json['section']).map((e) => Section.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
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> data;
|
||||
|
||||
Section.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
classTotal = json['class_total'];
|
||||
data = List.from(json['data']).map((e) => Data.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
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<String, dynamic> json) {
|
||||
userId = json['user_id'];
|
||||
_value = json['value'];
|
||||
value_type = json['value_type'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['user_id'] = userId;
|
||||
data['value'] = _value;
|
||||
data['value_type'] = value_type;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OneConstructUse> 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<void> removeEdittedUses(
|
||||
// List<String> 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(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -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<LemmaConstructsModel> 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<String, dynamic> json) {
|
||||
// try {
|
||||
// debugger(
|
||||
// when:
|
||||
// kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null),
|
||||
// );
|
||||
return ConstructAnalyticsModel(
|
||||
// lemma: json[ModelKey.lemma],
|
||||
// uses: (json['uses'] as Iterable)
|
||||
// .map<OneConstructUse?>(
|
||||
// (use) => use != null ? OneConstructUse.fromJson(use) : null,
|
||||
// )
|
||||
// .where((element) => element != null)
|
||||
// .cast<OneConstructUse>()
|
||||
// .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<LemmaConstructsModel>()
|
||||
.toList(),
|
||||
);
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
toJson() {
|
||||
final Map<String, dynamic> 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<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// if (use.lemma != lemma) {
|
||||
// throw Exception('lemma mismatch');
|
||||
// }
|
||||
// uses.add(use);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
class LemmaConstructsModel {
|
||||
String lemma;
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
LemmaConstructsModel({
|
||||
required this.lemma,
|
||||
this.uses = const [],
|
||||
});
|
||||
|
||||
factory LemmaConstructsModel.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaConstructsModel(
|
||||
lemma: json[ModelKey.lemma],
|
||||
uses: (json['uses'] ?? [] as Iterable)
|
||||
.map<OneConstructUse?>(
|
||||
(use) => use != null ? OneConstructUse.fromJson(use) : null,
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RecentMessageRecord> _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<void> removeEdittedMessages(
|
||||
// RecentMessageRecord message,
|
||||
// ) async {
|
||||
// final List<String> 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<void> 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<void> bulkUpdate(List<RecentMessageRecord> 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<void> _updateStudentAnalytics() async {
|
||||
// // content.lastUpdated = DateTime.now();
|
||||
// // content.addAll(_messagesToSave);
|
||||
// // _clearMessages();
|
||||
|
||||
// // await event.room.client.setRoomStateWithKey(
|
||||
// // event.room.id,
|
||||
// // _event.type,
|
||||
// // '',
|
||||
// // content.toJson(),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// Future<void> 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<TimeSeriesTotals> 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<TimeSeriesInterval> 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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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<StudentAnalyticsSummary>();
|
||||
// return _contentCache!;
|
||||
// }
|
||||
|
||||
// List<TimeSeriesInterval> get monthly => _content.monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> get daily => _content.dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> 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() {}
|
||||
// }
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RecentMessageRecord> _messages;
|
||||
|
||||
SummaryAnalyticsModel({
|
||||
required List<RecentMessageRecord> messages,
|
||||
super.lastUpdated,
|
||||
super.prevEventId,
|
||||
super.prevLastUpdated,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
static const _lastUpdatedKey = "lupt";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
_lastUpdatedKey: lastUpdated?.toIso8601String(),
|
||||
ModelKey.prevEventId: prevEventId,
|
||||
ModelKey.prevLastUpdated: prevLastUpdated?.toIso8601String(),
|
||||
};
|
||||
|
||||
factory SummaryAnalyticsModel.fromJson(json) {
|
||||
List<RecentMessageRecord> savedMessages = [];
|
||||
try {
|
||||
savedMessages = json[_messagesKey] != null
|
||||
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
|
||||
.map((e) => RecentMessageRecord.fromJson(e))
|
||||
.toList()
|
||||
.cast<RecentMessageRecord>()
|
||||
: [];
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BaseAnalyticsPage> {
|
|||
|
||||
final List<AnalyticsEvent> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ConstructListView> {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<MessagesBarChart> {
|
|||
getTitlesWidget: leftTitles,
|
||||
),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue