refactoring of my analytics controller and related flows

pull/1384/head
William Jordan-Cooley 1 year ago
parent ffbc62ba55
commit 8ceb7851e5

@ -3109,7 +3109,7 @@
"prettyGood": "Pretty good! Here's what I would have said.", "prettyGood": "Pretty good! Here's what I would have said.",
"letMeThink": "Hmm, let's see how you did!", "letMeThink": "Hmm, let's see how you did!",
"clickMessageTitle": "Need help?", "clickMessageTitle": "Need help?",
"clickMessageBody": "Click messages to access definitions, translations, and audio!", "clickMessageBody": "Click a message for language help! Click and hold to react 😀.",
"understandingMessagesTitle": "Definitions and translations!", "understandingMessagesTitle": "Definitions and translations!",
"understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).", "understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).",
"allDone": "All done!", "allDone": "All done!",

@ -4512,7 +4512,7 @@
"definitions": "definiciones", "definitions": "definiciones",
"subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como", "subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como",
"clickMessageTitle": "¿Necesitas ayuda?", "clickMessageTitle": "¿Necesitas ayuda?",
"clickMessageBody": "Haga clic en los mensajes para acceder a las definiciones, traducciones y audio.", "clickMessageBody": "¡Lame un mensaje para obtener ayuda con el idioma! Haz clic y mantén presionado para reaccionar 😀",
"more": "Más", "more": "Más",
"translationTooltip": "Traducir", "translationTooltip": "Traducir",
"audioTooltip": "Reproducir audio", "audioTooltip": "Reproducir audio",

@ -917,7 +917,7 @@ class ChatListController extends State<ChatList>
if (mounted) { if (mounted) {
GoogleAnalytics.analyticsUserUpdate(client.userID); GoogleAnalytics.analyticsUserUpdate(client.userID);
await pangeaController.subscriptionController.initialize(); await pangeaController.subscriptionController.initialize();
await pangeaController.myAnalytics.addEventsListener(); await pangeaController.myAnalytics.initialize();
pangeaController.afterSyncAndFirstLoginInitialization(context); pangeaController.afterSyncAndFirstLoginInitialization(context);
await pangeaController.inviteBotToExistingSpaces(); await pangeaController.inviteBotToExistingSpaces();
await pangeaController.setPangeaPushRules(); await pangeaController.setPangeaPushRules();

@ -1,16 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
<<<<<<< Updated upstream
import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart';
=======
>>>>>>> Stashed changes
import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.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/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_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/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:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -18,11 +19,18 @@ import 'package:matrix/matrix.dart';
import '../extensions/client_extension/client_extension.dart'; import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart';
// controls the sending of analytics events /// handles the processing of analytics for
class MyAnalyticsController extends BaseController { /// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
class MyAnalyticsController {
late PangeaController _pangeaController; late PangeaController _pangeaController;
Timer? _updateTimer; Timer? _updateTimer;
/// the max number of messages that will be cached before
/// an automatic update is triggered
final int _maxMessagesCached = 10; final int _maxMessagesCached = 10;
/// the number of minutes before an automatic update is triggered
final int _minutesBeforeUpdate = 5; final int _minutesBeforeUpdate = 5;
/// the time since the last update that will trigger an automatic update /// the time since the last update that will trigger an automatic update
@ -33,41 +41,50 @@ class MyAnalyticsController extends BaseController {
} }
/// adds the listener that handles when to run automatic updates /// adds the listener that handles when to run automatic updates
/// to analytics - either after a certain number of messages sent/ /// to analytics - either after a certain number of messages sent
/// received or after a certain amount of time [_timeSinceUpdate] without an update /// received or after a certain amount of time [_timeSinceUpdate] without an update
Future<void> addEventsListener() async { Future<void> initialize() async {
final Client client = _pangeaController.matrixState.client; final lastUpdated = await _refreshAnalyticsIfOutdated();
// listen for new messages and updateAnalytics timer
// we are doing this in an attempt to update analytics when activitiy is low
// both in messages sent by this client and other clients that you're connected with
// doesn't account for messages sent by other clients that you're not connected with
_client.onSync.stream
.where((SyncUpdate update) => update.rooms?.join != null)
.listen((update) {
updateAnalyticsTimer(update, lastUpdated);
});
}
// if analytics haven't been updated in the last day, update them /// If analytics haven't been updated in the last day, update them
Future<DateTime?> _refreshAnalyticsIfOutdated() async {
DateTime? lastUpdated = await _pangeaController.analytics DateTime? lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate); final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
if (lastUpdated?.isBefore(yesterday) ?? true) { if (lastUpdated?.isBefore(yesterday) ?? true) {
debugPrint("analytics out-of-date, updating"); debugPrint("analytics out-of-date, updating");
await updateAnalytics(); await updateAnalytics();
lastUpdated = await _pangeaController.analytics lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
} }
return lastUpdated;
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 Client get _client => _pangeaController.matrixState.client;
/// Given an update from sync stream, check if the update contains
/// messages for which analytics will be saved. If so, reset the timer /// 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 /// and add the event ID to the cache of un-added event IDs
void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) { void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) {
for (final entry in update.rooms!.join!.entries) { for (final entry in update.rooms!.join!.entries) {
final Room room = final Room room = _client.getRoomById(entry.key)!;
_pangeaController.matrixState.client.getRoomById(entry.key)!;
// get the new events in this sync that are messages // get the new events in this sync that are messages
final List<Event>? events = entry.value.timeline?.events final List<Event>? events = entry.value.timeline?.events
?.map((event) => Event.fromMatrixEvent(event, room)) ?.map((event) => Event.fromMatrixEvent(event, room))
.where((event) => eventHasAnalytics(event, lastUpdated)) .where((event) => hasUserAnalyticsToCache(event, lastUpdated))
.toList(); .toList();
// add their event IDs to the cache of un-added event IDs // add their event IDs to the cache of un-added event IDs
@ -87,8 +104,9 @@ class MyAnalyticsController extends BaseController {
} }
// checks if event from sync update is a message that should have analytics // checks if event from sync update is a message that should have analytics
bool eventHasAnalytics(Event event, DateTime? lastUpdated) { bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) {
return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) && return event.senderId == _client.userID &&
(lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
event.type == EventTypes.Message && event.type == EventTypes.Message &&
event.messageType == MessageTypes.Text && event.messageType == MessageTypes.Text &&
!(event.eventId.contains("web") && !(event.eventId.contains("web") &&
@ -176,21 +194,18 @@ class MyAnalyticsController extends BaseController {
} }
} }
String? get userL2 => _pangeaController.languageController.activeL2Code();
// top level analytics sending function. Send analytics // top level analytics sending function. Send analytics
// for each type of analytics event // for each type of analytics event
// to each of the applicable analytics rooms // to each of the applicable analytics rooms
Future<void> _updateAnalytics() async { Future<void> _updateAnalytics() async {
// if the user's l2 is not sent, don't send analytics // if missing important info, don't send analytics
final String? userL2 = _pangeaController.languageController.activeL2Code(); if (userL2 == null || _client.userID == null) {
if (userL2 == null) { debugger(when: kDebugMode);
return; return;
} }
// 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 the last updated time for each analytics room // get the last updated time for each analytics room
// and the least recent update, which will be used to determine // and the least recent update, which will be used to determine
// how far to go back in the chat history to get messages // how far to go back in the chat history to get messages
@ -217,151 +232,128 @@ class MyAnalyticsController extends BaseController {
lastUpdates.isNotEmpty ? lastUpdates.first : null; lastUpdates.isNotEmpty ? lastUpdates.first : null;
} }
// for each chat the user is studying in, get all the messages final List<Room> chats = await _client.chatsImAStudentIn;
// since the least recent update analytics update, and sort them
// by their langCodes final List<PangeaMessageEvent> recentMsgs =
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs = await _getMessagesWithUnsavedAnalytics(
await getLangCodesToMsgs(
userL2,
l2AnalyticsLastUpdated, l2AnalyticsLastUpdated,
chats,
);
final List<ActivityRecordResponse> recentActivities =
await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats);
// FOR DISCUSSION:
// we want to make sure we save something for every message send
// however, we're currently saving analytics for messages not in the userL2
// based on bad language detection results. maybe it would be better to
// save the analytics for these messages in the userL2 analytics room, but
// with useType of unknown
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
// if there is no analytics room for this langCode, then user hadn't sent
// message in this language at the time of the last analytics update
// so fallback to the least recent update time
final DateTime? lastUpdated =
lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated;
// final String msgLangCode = (msg.originalSent?.langCode != null &&
// msg.originalSent?.langCode != LanguageKeys.unknownLanguage)
// ? msg.originalSent!.langCode
// : userL2;
// finally, send the analytics events to the analytics room
await _sendAnalyticsEvents(
analyticsRoom,
recentMsgs,
lastUpdated,
recentActivities,
); );
}
final List<String> langCodes = langCodeToMsgs.keys.toList(); Future<List<ActivityRecordResponse>> getRecentActivities(
for (final String langCode in langCodes) { String userL2,
// for each of the langs that the user has sent message in, get DateTime? lastUpdated,
// the corresponding analytics room (or create it) List<Room> chats,
final Room analyticsRoom = await _pangeaController.matrixState.client ) async {
.getMyAnalyticsRoom(langCode); final List<Future<List<Event>>> recentActivityFutures = [];
for (final Room chat in chats) {
// if there is no analytics room for this langCode, then user hadn't sent recentActivityFutures.add(
// message in this language at the time of the last analytics update chat.getEventsBySender(
// so fallback to the least recent update time type: PangeaEventTypes.activityRecord,
final DateTime? lastUpdated = sender: _client.userID!,
lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; since: lastUpdated,
),
// get the corresponding list of recent messages for this langCode
final List<PangeaMessageEvent> recentMsgs =
langCodeToMsgs[langCode] ?? [];
// finally, send the analytics events to the analytics room
await sendAnalyticsEvents(
analyticsRoom,
recentMsgs,
lastUpdated,
); );
} }
final List<List<Event>> recentActivityLists =
await Future.wait(recentActivityFutures);
return recentActivityLists
.expand((e) => e)
.map((e) => ActivityRecordResponse.fromJson(e.content))
.toList();
} }
Future<Map<String, List<PangeaMessageEvent>>> getLangCodesToMsgs( /// Returns the new messages that have not yet been saved to analytics.
String userL2, /// The keys in the map correspond to different categories or groups of messages,
/// while the values are lists of [PangeaMessageEvent] objects belonging to each category.
Future<List<PangeaMessageEvent>> _getMessagesWithUnsavedAnalytics(
DateTime? since, DateTime? since,
List<Room> chats,
) async { ) async {
// get a map of langCodes to messages for each chat the user is studying in // get the recent messages for each chat
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs = {}; final List<Future<List<PangeaMessageEvent>>> futures = [];
for (final Room chat in _studentChats) { for (final Room chat in chats) {
List<PangeaMessageEvent>? recentMsgs; futures.add(
try { chat.myMessageEventsInChat(
recentMsgs = await chat.myMessageEventsInChat(
since: since, since: since,
); ),
} catch (err) { );
debugPrint("failed to fetch messages for chat ${chat.id}");
continue;
}
// sort those messages by their langCode
// langCode is hopefully based on the original sent rep, but if that
// is null or unk, it will be based on the user's current l2
for (final msg in recentMsgs) {
final String msgLangCode = (msg.originalSent?.langCode != null &&
msg.originalSent?.langCode != LanguageKeys.unknownLanguage)
? msg.originalSent!.langCode
: userL2;
langCodeToMsgs[msgLangCode] ??= [];
langCodeToMsgs[msgLangCode]!.add(msg);
}
} }
return langCodeToMsgs; final List<List<PangeaMessageEvent>> recentMsgLists =
await Future.wait(futures);
// flatten the list of lists of messages
return recentMsgLists.expand((e) => e).toList();
} }
Future<void> sendAnalyticsEvents( Future<void> _sendAnalyticsEvents(
Room analyticsRoom, Room analyticsRoom,
List<PangeaMessageEvent> recentMsgs, List<PangeaMessageEvent> recentMsgs,
DateTime? lastUpdated, DateTime? lastUpdated,
List<ActivityRecordResponse> recentActivities,
) async { ) async {
// remove messages that were sent before the last update final List<OneConstructUse> constructContent = [];
if (recentMsgs.isEmpty) return;
if (lastUpdated != null) { if (recentMsgs.isNotEmpty) {
recentMsgs.removeWhere( // remove messages that were sent before the last update
(msg) => msg.event.originServerTs.isBefore(lastUpdated),
); // format the analytics data
} final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || lastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
);
}
// format the analytics data constructContent
final List<RecentMessageRecord> summaryContent = .addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs));
SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
final List<OneConstructUse> constructContent =
ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || lastUpdated == null) {
await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
analyticsRoom,
summaryContent,
);
} }
if (constructContent.isNotEmpty) { if (recentActivities.isNotEmpty) {
await ConstructAnalyticsEvent.sendConstructsEvent( // TODO - Concert recentActivities into list of constructUse objects.
analyticsRoom, // First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we
constructContent, // could search for completed practice activities and see which have been completed by the user.
); // It's not clear which is the best approach at the moment and we should consider both.
} }
}
List<Room> _studentChats = []; await analyticsRoom.sendConstructsEvent(
constructContent,
Future<void> setStudentChats() async { );
final List<String> teacherRoomIds =
await _pangeaController.matrixState.client.teacherRoomIds;
_studentChats = _pangeaController.matrixState.client.rooms
.where(
(r) =>
!r.isSpace &&
!r.isAnalyticsRoom &&
!teacherRoomIds.contains(r.id),
)
.toList();
setState(data: _studentChats);
}
List<Room> get studentChats {
try {
if (_studentChats.isNotEmpty) return _studentChats;
setStudentChats();
return _studentChats;
} catch (err) {
debugger(when: kDebugMode);
return [];
}
}
List<Room> _studentSpaces = [];
Future<void> setStudentSpaces() async {
_studentSpaces =
await _pangeaController.matrixState.client.spacesImStudyingIn;
}
List<Room> get studentSpaces {
try {
if (_studentSpaces.isNotEmpty) return _studentSpaces;
setStudentSpaces();
return _studentSpaces;
} catch (err) {
debugger(when: kDebugMode);
return [];
}
} }
} }

@ -51,7 +51,9 @@ extension PangeaClient on Client {
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching; Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching;
Future<List<Room>> get spacesImStudyingIn async => await _spacesImStudyingIn; Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn;
List<Room> get spacesImIn => _spacesImIn; List<Room> get spacesImIn => _spacesImIn;

@ -19,6 +19,18 @@ extension SpaceClientExtension on Client {
return spaces; return spaces;
} }
Future<List<Room>> get _chatsImAStudentIn async {
final List<String> nowteacherRoomIds = await teacherRoomIds;
return rooms
.where(
(r) =>
!r.isSpace &&
!r.isAnalyticsRoom &&
!nowteacherRoomIds.contains(r.id),
)
.toList();
}
Future<List<Room>> get _spacesImStudyingIn async { Future<List<Room>> get _spacesImStudyingIn async {
final List<Room> joinedSpaces = rooms final List<Room> joinedSpaces = rooms
.where( .where(

@ -429,21 +429,27 @@ extension EventsRoomExtension on Room {
Future<List<PangeaMessageEvent>> myMessageEventsInChat({ Future<List<PangeaMessageEvent>> myMessageEventsInChat({
DateTime? since, DateTime? since,
}) async { }) async {
final List<Event> msgEvents = await getEventsBySender( try {
type: EventTypes.Message, final List<Event> msgEvents = await getEventsBySender(
sender: client.userID!, type: EventTypes.Message,
since: since, 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(); 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();
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return [];
}
} }
// fetch event of a certain type by a certain sender // fetch event of a certain type by a certain sender

@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/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_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_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.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/bot_options_model.dart';

@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
return; return;
} }
for (final Room space in (await client.spacesImStudyingIn)) { for (final Room space in (await client.spaceImAStudentIn)) {
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
await space.addAnalyticsRoomToSpace(this); await space.addAnalyticsRoomToSpace(this);
} }
@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room {
return; return;
} }
for (final Room space in (await client.spacesImStudyingIn)) { for (final Room space in (await client.spaceImAStudentIn)) {
await space.inviteSpaceTeachersToAnalyticsRoom(this); await space.inviteSpaceTeachersToAnalyticsRoom(this);
} }
} }
@ -249,4 +249,34 @@ extension AnalyticsRoomExtension on Room {
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode || return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode; creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
} }
Future<String?> sendSummaryAnalyticsEvent(
List<RecentMessageRecord> records,
) async {
if (records.isEmpty) return null;
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: records,
);
final String? eventId = await sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
return eventId;
}
Future<String?> sendConstructsEvent(
List<OneConstructUse> uses,
) async {
if (uses.isEmpty) return null;
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
uses: uses,
);
final String? eventId = await sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
return eventId;
}
} }

@ -1,8 +1,6 @@
import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.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/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:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -28,32 +26,4 @@ abstract class AnalyticsEvent {
} }
return contentCache!; 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;
}
} }

@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
contentCache ??= ConstructAnalyticsModel.fromJson(event.content); contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
return contentCache as ConstructAnalyticsModel; return contentCache as ConstructAnalyticsModel;
} }
static Future<String?> sendConstructsEvent(
Room analyticsRoom,
List<OneConstructUse> uses,
) async {
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
uses: uses,
);
final String? eventId = await analyticsRoom.sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
return eventId;
}
} }

@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent {
contentCache ??= SummaryAnalyticsModel.fromJson(event.content); contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
return contentCache as SummaryAnalyticsModel; 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;
}
} }

@ -7,14 +7,14 @@ import 'dart:typed_data';
class PracticeActivityRecordModel { class PracticeActivityRecordModel {
final String? question; final String? question;
late List<ActivityResponse> responses; late List<ActivityRecordResponse> responses;
PracticeActivityRecordModel({ PracticeActivityRecordModel({
required this.question, required this.question,
List<ActivityResponse>? responses, List<ActivityRecordResponse>? responses,
}) { }) {
if (responses == null) { if (responses == null) {
this.responses = List<ActivityResponse>.empty(growable: true); this.responses = List<ActivityRecordResponse>.empty(growable: true);
} else { } else {
this.responses = responses; this.responses = responses;
} }
@ -26,7 +26,9 @@ class PracticeActivityRecordModel {
return PracticeActivityRecordModel( return PracticeActivityRecordModel(
question: json['question'] as String, question: json['question'] as String,
responses: (json['responses'] as List) responses: (json['responses'] as List)
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>)) .map(
(e) => ActivityRecordResponse.fromJson(e as Map<String, dynamic>),
)
.toList(), .toList(),
); );
} }
@ -55,7 +57,7 @@ class PracticeActivityRecordModel {
}) { }) {
try { try {
responses.add( responses.add(
ActivityResponse( ActivityRecordResponse(
text: text, text: text,
audioBytes: audioBytes, audioBytes: audioBytes,
imageBytes: imageBytes, imageBytes: imageBytes,
@ -84,7 +86,7 @@ class PracticeActivityRecordModel {
int get hashCode => question.hashCode ^ responses.hashCode; int get hashCode => question.hashCode ^ responses.hashCode;
} }
class ActivityResponse { class ActivityRecordResponse {
// the user's response // the user's response
// has nullable string, nullable audio bytes, nullable image bytes, and timestamp // has nullable string, nullable audio bytes, nullable image bytes, and timestamp
final String? text; final String? text;
@ -92,15 +94,15 @@ class ActivityResponse {
final Uint8List? imageBytes; final Uint8List? imageBytes;
final DateTime timestamp; final DateTime timestamp;
ActivityResponse({ ActivityRecordResponse({
this.text, this.text,
this.audioBytes, this.audioBytes,
this.imageBytes, this.imageBytes,
required this.timestamp, required this.timestamp,
}); });
factory ActivityResponse.fromJson(Map<String, dynamic> json) { factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
return ActivityResponse( return ActivityRecordResponse(
text: json['text'] as String?, text: json['text'] as String?,
audioBytes: json['audio'] as Uint8List?, audioBytes: json['audio'] as Uint8List?,
imageBytes: json['image'] as Uint8List?, imageBytes: json['image'] as Uint8List?,
@ -121,7 +123,7 @@ class ActivityResponse {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other is ActivityResponse && return other is ActivityRecordResponse &&
other.text == text && other.text == text &&
other.audioBytes == audioBytes && other.audioBytes == audioBytes &&
other.imageBytes == imageBytes && other.imageBytes == imageBytes &&

@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart';
@ -29,49 +28,35 @@ class StudentAnalyticsPage extends StatefulWidget {
class StudentAnalyticsController extends State<StudentAnalyticsPage> { class StudentAnalyticsController extends State<StudentAnalyticsPage> {
final PangeaController _pangeaController = MatrixState.pangeaController; final PangeaController _pangeaController = MatrixState.pangeaController;
AnalyticsSelected? selected; AnalyticsSelected? selected;
StreamSubscription? stateSub;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final listFutures = [
_pangeaController.myAnalytics.setStudentChats(),
_pangeaController.myAnalytics.setStudentSpaces(),
];
Future.wait(listFutures).then((_) => setState(() {}));
stateSub = _pangeaController.myAnalytics.stateStream.listen((_) {
setState(() {});
});
} }
@override @override
void dispose() { void dispose() {
stateSub?.cancel();
super.dispose(); super.dispose();
} }
List<Room> _chats = [];
List<Room> get chats { List<Room> get chats {
if (_pangeaController.myAnalytics.studentChats.isEmpty) { if (_chats.isEmpty) {
_pangeaController.myAnalytics.setStudentChats().then((_) { _pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
if (_pangeaController.myAnalytics.studentChats.isNotEmpty) { setState(() => _chats = result);
setState(() {});
}
}); });
} }
return _pangeaController.myAnalytics.studentChats; return _chats;
} }
List<Room> _spaces = [];
List<Room> get spaces { List<Room> get spaces {
if (_pangeaController.myAnalytics.studentSpaces.isEmpty) { if (_spaces.isEmpty) {
_pangeaController.myAnalytics.setStudentSpaces().then((_) { _pangeaController.matrixState.client.spaceImAStudentIn.then((result) {
if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) { setState(() => _spaces = result);
setState(() {});
}
}); });
} }
return _pangeaController.myAnalytics.studentSpaces; return _spaces;
} }
String? get userId { String? get userId {

Loading…
Cancel
Save