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.",
"letMeThink": "Hmm, let's see how you did!",
"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!",
"understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).",
"allDone": "All done!",

@ -4512,7 +4512,7 @@
"definitions": "definiciones",
"subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como",
"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",
"translationTooltip": "Traducir",
"audioTooltip": "Reproducir audio",

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

@ -1,16 +1,17 @@
import 'dart:async';
import 'dart:developer';
<<<<<<< Updated upstream
import 'package:fluffychat/pangea/constants/language_constants.dart';
=======
>>>>>>> Stashed changes
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/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/summary_analytics_event.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:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
@ -18,11 +19,18 @@ import 'package:matrix/matrix.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
// controls the sending of analytics events
class MyAnalyticsController extends BaseController {
/// handles the processing of analytics for
/// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
class MyAnalyticsController {
late PangeaController _pangeaController;
Timer? _updateTimer;
/// the max number of messages that will be cached before
/// an automatic update is triggered
final int _maxMessagesCached = 10;
/// the number of minutes before an automatic update is triggered
final int _minutesBeforeUpdate = 5;
/// 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
/// 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
Future<void> addEventsListener() async {
final Client client = _pangeaController.matrixState.client;
Future<void> initialize() async {
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
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
if (lastUpdated?.isBefore(yesterday) ?? true) {
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);
});
return 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
/// 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)!;
final Room room = _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))
.where((event) => hasUserAnalyticsToCache(event, lastUpdated))
.toList();
// 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
bool eventHasAnalytics(Event event, DateTime? lastUpdated) {
return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) {
return event.senderId == _client.userID &&
(lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
event.type == EventTypes.Message &&
event.messageType == MessageTypes.Text &&
!(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
// for each type of analytics event
// to each of the applicable analytics rooms
Future<void> _updateAnalytics() async {
// if the user's l2 is not sent, don't send analytics
final String? userL2 = _pangeaController.languageController.activeL2Code();
if (userL2 == null) {
// if missing important info, don't send analytics
if (userL2 == null || _client.userID == null) {
debugger(when: kDebugMode);
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
// and the least recent update, which will be used to determine
// how far to go back in the chat history to get messages
@ -217,21 +232,25 @@ class MyAnalyticsController extends BaseController {
lastUpdates.isNotEmpty ? lastUpdates.first : null;
}
// for each chat the user is studying in, get all the messages
// since the least recent update analytics update, and sort them
// by their langCodes
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs =
await getLangCodesToMsgs(
userL2,
final List<Room> chats = await _client.chatsImAStudentIn;
final List<PangeaMessageEvent> recentMsgs =
await _getMessagesWithUnsavedAnalytics(
l2AnalyticsLastUpdated,
chats,
);
final List<String> langCodes = langCodeToMsgs.keys.toList();
for (final String langCode in langCodes) {
// for each of the langs that the user has sent message in, get
// the corresponding analytics room (or create it)
final Room analyticsRoom = await _pangeaController.matrixState.client
.getMyAnalyticsRoom(langCode);
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
@ -239,129 +258,102 @@ class MyAnalyticsController extends BaseController {
final DateTime? lastUpdated =
lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated;
// get the corresponding list of recent messages for this langCode
final List<PangeaMessageEvent> recentMsgs =
langCodeToMsgs[langCode] ?? [];
// 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(
await _sendAnalyticsEvents(
analyticsRoom,
recentMsgs,
lastUpdated,
recentActivities,
);
}
}
Future<Map<String, List<PangeaMessageEvent>>> getLangCodesToMsgs(
Future<List<ActivityRecordResponse>> getRecentActivities(
String userL2,
DateTime? since,
DateTime? lastUpdated,
List<Room> chats,
) async {
// get a map of langCodes to messages for each chat the user is studying in
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs = {};
for (final Room chat in _studentChats) {
List<PangeaMessageEvent>? recentMsgs;
try {
recentMsgs = await chat.myMessageEventsInChat(
since: since,
final List<Future<List<Event>>> recentActivityFutures = [];
for (final Room chat in chats) {
recentActivityFutures.add(
chat.getEventsBySender(
type: PangeaEventTypes.activityRecord,
sender: _client.userID!,
since: lastUpdated,
),
);
} catch (err) {
debugPrint("failed to fetch messages for chat ${chat.id}");
continue;
}
final List<List<Event>> recentActivityLists =
await Future.wait(recentActivityFutures);
// 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 recentActivityLists
.expand((e) => e)
.map((e) => ActivityRecordResponse.fromJson(e.content))
.toList();
}
/// Returns the new messages that have not yet been saved to analytics.
/// 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,
List<Room> chats,
) async {
// get the recent messages for each chat
final List<Future<List<PangeaMessageEvent>>> futures = [];
for (final Room chat in chats) {
futures.add(
chat.myMessageEventsInChat(
since: since,
),
);
}
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,
List<PangeaMessageEvent> recentMsgs,
DateTime? lastUpdated,
List<ActivityRecordResponse> recentActivities,
) async {
final List<OneConstructUse> constructContent = [];
if (recentMsgs.isNotEmpty) {
// remove messages that were sent before the last update
if (recentMsgs.isEmpty) return;
if (lastUpdated != null) {
recentMsgs.removeWhere(
(msg) => msg.event.originServerTs.isBefore(lastUpdated),
);
}
// format the analytics data
final List<RecentMessageRecord> summaryContent =
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,
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
);
}
if (constructContent.isNotEmpty) {
await ConstructAnalyticsEvent.sendConstructsEvent(
analyticsRoom,
constructContent,
);
}
constructContent
.addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs));
}
List<Room> _studentChats = [];
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);
if (recentActivities.isNotEmpty) {
// TODO - Concert recentActivities into list of constructUse objects.
// First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we
// 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> 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 [];
}
await analyticsRoom.sendConstructsEvent(
constructContent,
);
}
}

@ -51,7 +51,9 @@ extension PangeaClient on Client {
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;

@ -19,6 +19,18 @@ extension SpaceClientExtension on Client {
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 {
final List<Room> joinedSpaces = rooms
.where(

@ -429,6 +429,7 @@ extension EventsRoomExtension on Room {
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
DateTime? since,
}) async {
try {
final List<Event> msgEvents = await getEventsBySender(
type: EventTypes.Message,
sender: client.userID!,
@ -444,6 +445,11 @@ extension EventsRoomExtension on Room {
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

@ -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/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/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';

@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
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;
await space.addAnalyticsRoomToSpace(this);
}
@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room {
return;
}
for (final Room space in (await client.spacesImStudyingIn)) {
for (final Room space in (await client.spaceImAStudentIn)) {
await space.inviteSpaceTeachersToAnalyticsRoom(this);
}
}
@ -249,4 +249,34 @@ extension AnalyticsRoomExtension on Room {
return creationContent?.tryGet<String>(ModelKey.langCode) == 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/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';
@ -28,32 +26,4 @@ abstract class AnalyticsEvent {
}
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);
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);
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 {
final String? question;
late List<ActivityResponse> responses;
late List<ActivityRecordResponse> responses;
PracticeActivityRecordModel({
required this.question,
List<ActivityResponse>? responses,
List<ActivityRecordResponse>? responses,
}) {
if (responses == null) {
this.responses = List<ActivityResponse>.empty(growable: true);
this.responses = List<ActivityRecordResponse>.empty(growable: true);
} else {
this.responses = responses;
}
@ -26,7 +26,9 @@ class PracticeActivityRecordModel {
return PracticeActivityRecordModel(
question: json['question'] as String,
responses: (json['responses'] as List)
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>))
.map(
(e) => ActivityRecordResponse.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
}
@ -55,7 +57,7 @@ class PracticeActivityRecordModel {
}) {
try {
responses.add(
ActivityResponse(
ActivityRecordResponse(
text: text,
audioBytes: audioBytes,
imageBytes: imageBytes,
@ -84,7 +86,7 @@ class PracticeActivityRecordModel {
int get hashCode => question.hashCode ^ responses.hashCode;
}
class ActivityResponse {
class ActivityRecordResponse {
// the user's response
// has nullable string, nullable audio bytes, nullable image bytes, and timestamp
final String? text;
@ -92,15 +94,15 @@ class ActivityResponse {
final Uint8List? imageBytes;
final DateTime timestamp;
ActivityResponse({
ActivityRecordResponse({
this.text,
this.audioBytes,
this.imageBytes,
required this.timestamp,
});
factory ActivityResponse.fromJson(Map<String, dynamic> json) {
return ActivityResponse(
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
return ActivityRecordResponse(
text: json['text'] as String?,
audioBytes: json['audio'] as Uint8List?,
imageBytes: json['image'] as Uint8List?,
@ -121,7 +123,7 @@ class ActivityResponse {
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ActivityResponse &&
return other is ActivityRecordResponse &&
other.text == text &&
other.audioBytes == audioBytes &&
other.imageBytes == imageBytes &&

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

Loading…
Cancel
Save