From 9baabbe5b97912b358a6058fab3eb162856eacd2 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 19 Jul 2024 10:33:26 -0400 Subject: [PATCH 1/4] Changes criteria for showing translate warning --- .../widgets/chat/message_translation_card.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index fd80df134..8ea66094d 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class MessageTranslationCard extends StatefulWidget { final PangeaMessageEvent messageEvent; @@ -140,9 +141,15 @@ class MessageTranslationCardState extends State { return const CardErrorWidget(); } - final bool showWarning = l2Code != null && - !widget.immersionMode && - widget.messageEvent.originalSent?.langCode != l2Code && + // Show warning if message's language code is user's L1 + // or if translated text is same as original text + // Warning does not show if was previously closed + final bool showWarning = widget.messageEvent.originalSent != null && + ((!widget.immersionMode && + widget.messageEvent.originalSent!.langCode.equals(l1Code)) || + (selectionTranslation == null || + widget.messageEvent.originalSent!.text + .equals(selectionTranslation))) && !MatrixState.pangeaController.instructions.wereInstructionsTurnedOff( InlineInstructions.l1Translation.toString(), ); From 6378c59a2b38fc1471ceec6376772d733b7935a8 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 13:06:19 -0400 Subject: [PATCH 2/4] reduced the number of calls to postLoad and the number of awaits for analytics room management --- lib/pages/chat_list/chat_list.dart | 3 +- lib/pangea/controllers/class_controller.dart | 26 +-- .../message_analytics_controller.dart | 7 +- lib/pangea/controllers/pangea_controller.dart | 3 +- .../client_analytics_extension.dart | 152 ++++++------- .../client_extension/client_extension.dart | 31 +-- .../client_extension/space_extension.dart | 58 +---- .../events_extension.dart | 1 - .../pangea_room_extension.dart | 31 ++- .../room_analytics_extension.dart | 206 +++++++----------- .../space_settings_extension.dart | 32 ++- .../space_analytics/space_analytics.dart | 1 - .../student_analytics/student_analytics.dart | 11 +- .../utils/chat_list_handle_space_tap.dart | 3 +- lib/utils/client_manager.dart | 6 +- 15 files changed, 240 insertions(+), 331 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 803b773e1..ba350ee05 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -908,13 +908,14 @@ class ChatListController extends State // #Pangea if (mounted) { + // TODO try not to await so much GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); await pangeaController.myAnalytics.initialize(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); - await client.migrateAnalyticsRooms(); + client.migrateAnalyticsRooms(); } else { ErrorHandler.logError( m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 5fa59622d..84f17a5e4 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -31,18 +31,16 @@ class ClassController extends BaseController { setState(data: {"activeSpaceId": classId}); } - Future fixClassPowerLevels() async { - try { - final teacherSpaces = - await _pangeaController.matrixState.client.spacesImTeaching; - final List> classFixes = List.from(teacherSpaces) - .map((adminSpace) => adminSpace.setClassPowerLevels()) - .toList(); - await Future.wait(classFixes); - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } + /// For all the spaces that the user is teaching, set the power levels + /// to enable all other users to add child rooms to the space. + void fixClassPowerLevels() { + Future.wait( + _pangeaController.matrixState.client.spacesImTeaching.map( + (space) => space.setClassPowerLevels().catchError((err, s) { + ErrorHandler.logError(e: err, s: s); + }), + ), + ); } Future checkForClassCodeAndSubscription(BuildContext context) async { @@ -131,10 +129,10 @@ class ClassController extends BaseController { _pangeaController.matrixState.client.getRoomById(classChunk.roomId); // when possible, add user's analytics room the to space they joined - await joinedSpace?.addAnalyticsRoomsToSpace(); + joinedSpace?.addAnalyticsRoomsToSpace(); // and invite the space's teachers to the user's analytics rooms - await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); + joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); GoogleAnalytics.joinClass(classCode); return; } catch (err) { diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index f58a56698..32aaf66fc 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -198,9 +198,7 @@ class AnalyticsController extends BaseController { // gets all the summary analytics events for the students // in a space since the current timespace's cut off date - // ensure that all the space's events are loaded (mainly the for langCode) - // and that the participants are loaded - await space.postLoad(); + // ensure that the participants of the space are loaded await space.requestParticipants(); // TODO switch to using list of futures @@ -439,7 +437,6 @@ class AnalyticsController extends BaseController { timeSpan: currentAnalyticsTimeSpan, ); } - await space.postLoad(); } DateTime? lastUpdated; @@ -545,7 +542,6 @@ class AnalyticsController extends BaseController { Future> allSpaceMemberConstructs( Room space, ) async { - await space.postLoad(); await space.requestParticipants(); final List constructEvents = []; for (final student in space.students) { @@ -788,7 +784,6 @@ class AnalyticsController extends BaseController { ); return []; } - await space.postLoad(); } DateTime? lastUpdated; diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 7e24fa955..79222bbed 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -81,8 +81,7 @@ class PangeaController { BuildContext context, ) async { await classController.checkForClassCodeAndSubscription(context); - // startChatWithBotIfNotPresent(); - await classController.fixClassPowerLevels(); + classController.fixClassPowerLevels(); } /// Initialize controllers diff --git a/lib/pangea/extensions/client_extension/client_analytics_extension.dart b/lib/pangea/extensions/client_extension/client_analytics_extension.dart index fde4d8e54..396e09f8d 100644 --- a/lib/pangea/extensions/client_extension/client_analytics_extension.dart +++ b/lib/pangea/extensions/client_extension/client_analytics_extension.dart @@ -1,48 +1,40 @@ part of "client_extension.dart"; extension AnalyticsClientExtension on Client { - // get analytics room matching targetlanguage - // if not present, create it and invite teachers of that language - // set description to let people know what the hell it is + /// Get the logged in user's analytics room matching + /// a given langCode. If not present, create it. Future _getMyAnalyticsRoom(String langCode) async { - await roomsLoading; - // ensure room state events (room create, - // to check for analytics type) are loaded - for (final room in rooms) { - if (room.partial) await room.postLoad(); - } - - final Room? analyticsRoom = analyticsRoomLocal(langCode); - + final Room? analyticsRoom = _analyticsRoomLocal(langCode); if (analyticsRoom != null) return analyticsRoom; - return _makeAnalyticsRoom(langCode); } - //note: if langCode is null and user has >1 analyticsRooms then this could - //return the wrong one. this is to account for when an exchange might not - //be in a class. - Room? _analyticsRoomLocal(String? langCode, [String? userIdParam]) { + /// Get local analytics room for a given langCode and + /// optional userId (if not specified, uses current user). + /// If user is invited to the room, joins the room. + Room? _analyticsRoomLocal(String langCode, [String? userIdParam]) { final Room? analyticsRoom = rooms.firstWhereOrNull((e) { return e.isAnalyticsRoom && e.isAnalyticsRoomOfUser(userIdParam ?? userID!) && - (langCode != null ? e.isMadeForLang(langCode) : true); + e.isMadeForLang(langCode); }); if (analyticsRoom != null && analyticsRoom.membership == Membership.invite) { debugger(when: kDebugMode); - analyticsRoom - .join() - .onError( + analyticsRoom.join().onError( (error, stackTrace) => ErrorHandler.logError(e: error, s: stackTrace), - ) - .then((value) => analyticsRoom.postLoad()); + ); return analyticsRoom; } return analyticsRoom; } + /// Creates an analytics room with the specified language code and returns the created room. + /// Additionally, the room is added to the user's spaces and all teachers are invited to the room. + /// + /// If the room does not appear immediately after creation, this method waits for it to appear in sync. + /// Returns the created [Room] object. Future _makeAnalyticsRoom(String langCode) async { final String roomID = await createRoom( creationContent: { @@ -53,7 +45,6 @@ extension AnalyticsClientExtension on Client { topic: "This room stores learning analytics for $userID.", invite: [ ...(await myTeachers).map((e) => e.id), - // BotName.localBot, BotName.byEnvironment, ], ); @@ -66,14 +57,14 @@ extension AnalyticsClientExtension on Client { // add this analytics room to all spaces so teachers can join them // via the space hierarchy - await analyticsRoom?.addAnalyticsRoomToSpaces(); + analyticsRoom?.addAnalyticsRoomToSpaces(); // and invite all teachers to new analytics room - await analyticsRoom?.inviteTeachersToAnalyticsRoom(); + analyticsRoom?.inviteTeachersToAnalyticsRoom(); return getRoomById(roomID)!; } - // Get all my analytics rooms + /// Get all my analytics rooms List get _allMyAnalyticsRooms => rooms .where( (e) => e.isAnalyticsRoomOfUser(userID!), @@ -83,76 +74,77 @@ extension AnalyticsClientExtension on Client { // migration function to change analytics rooms' vsibility to public // so they will appear in the space hierarchy Future _updateAnalyticsRoomVisibility() async { - final List makePublicFutures = []; - for (final Room room in allMyAnalyticsRooms) { - final visability = await getRoomVisibilityOnDirectory(room.id); - if (visability != Visibility.public) { - await setRoomVisibilityOnDirectory( - room.id, - visibility: Visibility.public, - ); - } - } - await Future.wait(makePublicFutures); + await Future.wait( + allMyAnalyticsRooms.map((room) async { + final visability = await getRoomVisibilityOnDirectory(room.id); + if (visability != Visibility.public) { + await setRoomVisibilityOnDirectory( + room.id, + visibility: Visibility.public, + ); + } + }), + ); } - // Add all the users' analytics room to all the spaces the student studies in - // So teachers can join them via space hierarchy - // Will not always work, as there may be spaces where students don't have permission to add chats - // But allows teachers to join analytics rooms without being invited - Future _addAnalyticsRoomsToAllSpaces() async { - final List addFutures = []; + /// Add all the users' analytics room to all the spaces the user is studying in + /// so teachers can join them via space hierarchy. + /// Allows teachers to join analytics rooms without being invited. + void _addAnalyticsRoomsToAllSpaces() { for (final Room room in allMyAnalyticsRooms) { - addFutures.add(room.addAnalyticsRoomToSpaces()); + room.addAnalyticsRoomToSpaces(); } - await Future.wait(addFutures); } - // Invite teachers to all my analytics room - // Handles case when students cannot add analytics room to space(s) - // So teacher is still able to get analytics data for this student - Future _inviteAllTeachersToAllAnalyticsRooms() async { - final List inviteFutures = []; - for (final Room analyticsRoom in allMyAnalyticsRooms) { - inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom()); + /// Invite teachers to all my analytics room. + /// Handles case when students cannot add analytics room to space(s) + /// so teacher is still able to get analytics data for this student + void _inviteAllTeachersToAllAnalyticsRooms() { + for (final Room room in allMyAnalyticsRooms) { + room.inviteTeachersToAnalyticsRoom(); } - await Future.wait(inviteFutures); } // Join all analytics rooms in all spaces // Allows teachers to join analytics rooms without being invited Future _joinAnalyticsRoomsInAllSpaces() async { - final List joinFutures = []; - for (final Room space in (await _spacesImTeaching)) { - joinFutures.add(space.joinAnalyticsRoomsInSpace()); + for (final Room space in _spacesImTeaching) { + // Each call to joinAnalyticsRoomsInSpace calls getSpaceHierarchy, which has a + // strict rate limit. So we wait a second between each call to prevent a 429 error. + await Future.delayed( + const Duration(seconds: 1), + () => space.joinAnalyticsRoomsInSpace(), + ); } - await Future.wait(joinFutures); } - // Join invited analytics rooms - // Checks for invites to any student analytics rooms - // Handles case of analytics rooms that can't be added to some space(s) - Future _joinInvitedAnalyticsRooms() async { - final List allRooms = List.from(rooms); - for (final Room room in allRooms) { - if (room.membership == Membership.invite && room.isAnalyticsRoom) { - try { - await room.join(); - } catch (err) { - debugPrint("Failed to join analytics room ${room.id}"); - } - } - } + /// Join invited analytics rooms. + /// Checks for invites to any student analytics rooms. + /// Handles case of analytics rooms that can't be added to some space(s). + void _joinInvitedAnalyticsRooms() { + Future.wait( + rooms + .where( + (room) => + room.membership == Membership.invite && room.isAnalyticsRoom, + ) + .map( + (room) => room.join().catchError((err, s) { + ErrorHandler.logError(e: err, s: s); + }), + ), + ); } - // helper function to join all relevant analytics rooms - // and set up those rooms to be joined by relevant teachers - Future _migrateAnalyticsRooms() async { - await _updateAnalyticsRoomVisibility(); - await _addAnalyticsRoomsToAllSpaces(); - await _inviteAllTeachersToAllAnalyticsRooms(); - await _joinInvitedAnalyticsRooms(); - await _joinAnalyticsRoomsInAllSpaces(); + /// Helper function to join all relevant analytics rooms + /// and set up those rooms to be joined by other users. + void _migrateAnalyticsRooms() { + _updateAnalyticsRoomVisibility().then((_) { + _addAnalyticsRoomsToAllSpaces(); + _inviteAllTeachersToAllAnalyticsRooms(); + _joinInvitedAnalyticsRooms(); + _joinAnalyticsRoomsInAllSpaces(); + }); } Future> _allAnalyticsRoomsLastUpdated() async { diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index bef384f6a..af66c7d1f 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; @@ -20,10 +19,15 @@ part "space_extension.dart"; extension PangeaClient on Client { // analytics + /// Get the logged in user's analytics room matching + /// a given langCode. If not present, create it. Future getMyAnalyticsRoom(String langCode) async => await _getMyAnalyticsRoom(langCode); - Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) => + /// Get local analytics room for a given langCode and + /// optional userId (if not specified, uses current user). + /// If user is invited to the room, joins the room. + Room? analyticsRoomLocal(String langCode, [String? userIdParam]) => _analyticsRoomLocal(langCode, userIdParam); List get allMyAnalyticsRooms => _allMyAnalyticsRooms; @@ -31,35 +35,24 @@ extension PangeaClient on Client { Future updateAnalyticsRoomVisibility() async => await _updateAnalyticsRoomVisibility(); - Future addAnalyticsRoomsToAllSpaces() async => - await _addAnalyticsRoomsToAllSpaces(); - - Future inviteAllTeachersToAllAnalyticsRooms() async => - await _inviteAllTeachersToAllAnalyticsRooms(); - - Future joinAnalyticsRoomsInAllSpaces() async => - await _joinAnalyticsRoomsInAllSpaces(); - - Future joinInvitedAnalyticsRooms() async => - await _joinInvitedAnalyticsRooms(); - - Future migrateAnalyticsRooms() async => await _migrateAnalyticsRooms(); + /// Helper function to join all relevant analytics rooms + /// and set up those rooms to be joined by other users. + void migrateAnalyticsRooms() => _migrateAnalyticsRooms(); Future> allAnalyticsRoomsLastUpdated() async => await _allAnalyticsRoomsLastUpdated(); // spaces - Future> get spacesImTeaching async => await _spacesImTeaching; + List get spacesImTeaching => _spacesImTeaching; Future> get chatsImAStudentIn async => await _chatsImAStudentIn; - Future> get spaceImAStudentIn async => await _spacesImStudyingIn; + List get spacesImAStudentIn => _spacesImStudyingIn; List get spacesImIn => _spacesImIn; - Future get lastUpdatedRoomRules async => - await _lastUpdatedRoomRules; + PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules; // general_info diff --git a/lib/pangea/extensions/client_extension/space_extension.dart b/lib/pangea/extensions/client_extension/space_extension.dart index 0adc70469..89a09a7cd 100644 --- a/lib/pangea/extensions/client_extension/space_extension.dart +++ b/lib/pangea/extensions/client_extension/space_extension.dart @@ -1,23 +1,8 @@ part of "client_extension.dart"; extension SpaceClientExtension on Client { - Future> get _spacesImTeaching async { - final allSpaces = rooms.where((room) => room.isSpace); - for (final Room space in allSpaces) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - (e.isSpace) && - e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } + List get _spacesImTeaching => + rooms.where((e) => e.isSpace && e.isRoomAdmin).toList(); Future> get _chatsImAStudentIn async { final List nowteacherRoomIds = await teacherRoomIds; @@ -31,39 +16,18 @@ extension SpaceClientExtension on Client { .toList(); } - Future> get _spacesImStudyingIn async { - final List joinedSpaces = rooms - .where( - (room) => room.isSpace && room.membership == Membership.join, - ) - .toList(); - - for (final Room space in joinedSpaces) { - if (space.getState(EventTypes.RoomPowerLevels) == null) { - await space.postLoad(); - } - } - - final spaces = rooms - .where( - (e) => - e.isSpace && - e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin, - ) - .toList(); - return spaces; - } + List get _spacesImStudyingIn => + rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList(); List get _spacesImIn => rooms.where((e) => e.isSpace).toList(); - Future get _lastUpdatedRoomRules async => - (await _spacesImTeaching) - .where((space) => space.rulesUpdatedAt != null) - .sorted( - (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), - ) - .firstOrNull - ?.pangeaRoomRules; + PangeaRoomRules? get _lastUpdatedRoomRules => _spacesImTeaching + .where((space) => space.rulesUpdatedAt != null) + .sorted( + (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), + ) + .firstOrNull + ?.pangeaRoomRules; // LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms // .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null) diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index ce9d3451c..bc820998c 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -2,7 +2,6 @@ part of "pangea_room_extension.dart"; extension EventsRoomExtension on Room { Future _leaveIfFull() async { - await postLoad(); if (!isRoomAdmin && (_capacity != null) && (await _numNonAdmins) > (_capacity!)) { diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index fbec662a7..2ff1bf57d 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -49,26 +49,35 @@ part "user_permissions_extension.dart"; extension PangeaRoom on Room { // analytics + /// Join analytics rooms in space. + /// Allows teachers to join analytics rooms without being invited. Future joinAnalyticsRoomsInSpace() async => await _joinAnalyticsRoomsInSpace(); Future addAnalyticsRoomToSpace(Room analyticsRoom) async => await _addAnalyticsRoomToSpace(analyticsRoom); - Future addAnalyticsRoomToSpaces() async => - await _addAnalyticsRoomToSpaces(); + /// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces). + /// Enables teachers to join student analytics rooms via space hierarchy. + /// Will not always work, as there may be spaces where students don't have permission to add chats, + /// but allows teachers to join analytics rooms without being invited. + void addAnalyticsRoomToSpaces() => _addAnalyticsRoomToSpaces(); - Future addAnalyticsRoomsToSpace() async => - await _addAnalyticsRoomsToSpace(); + /// Add all the user's analytics rooms to 1 space. + void addAnalyticsRoomsToSpace() => _addAnalyticsRoomsToSpace(); + /// Invite teachers of 1 space to 1 analytics room Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - Future inviteTeachersToAnalyticsRoom() async => - await _inviteTeachersToAnalyticsRoom(); + /// Invite all the user's teachers to 1 analytics room. + /// Handles case when students cannot add analytics room to space + /// so teacher is still able to get analytics data for this student. + void inviteTeachersToAnalyticsRoom() => _inviteTeachersToAnalyticsRoom(); - Future inviteSpaceTeachersToAnalyticsRooms() async => - await _inviteSpaceTeachersToAnalyticsRooms(); + /// Invite teachers of 1 space to all users' analytics rooms + void inviteSpaceTeachersToAnalyticsRooms() => + _inviteSpaceTeachersToAnalyticsRooms(); Future getLastAnalyticsEvent( String type, @@ -147,6 +156,12 @@ extension PangeaRoom on Room { Future> get teachers async => await _teachers; + /// Synchronous version of teachers getter. Does not request + /// participants, so this list may not be complete. + List get teachersLocal => _teachersLocal; + + /// If the user is an admin of this space, and the space's + /// m.space.child power level hasn't yet been set, so it to 0 Future setClassPowerLevels() async => await _setClassPowerLevels(); Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index a27526a2b..fd1070e67 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -1,58 +1,45 @@ part of "pangea_room_extension.dart"; extension AnalyticsRoomExtension on Room { - // Join analytics rooms in space - // Allows teachers to join analytics rooms without being invited + /// Join analytics rooms in space. + /// Allows teachers to join analytics rooms without being invited. Future _joinAnalyticsRoomsInSpace() async { - if (!isSpace) { - debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "joinAnalyticsRoomsInSpace called on non-space room", - ), - ); - return; - } + try { + if (!isSpace) { + debugger(when: kDebugMode); + return; + } - // added delay because without it power levels don't load and user is not - // recognized as admin - await Future.delayed(const Duration(milliseconds: 500)); - await postLoad(); + if (!isRoomAdmin) return; + final spaceHierarchy = await client.getSpaceHierarchy( + id, + maxDepth: 1, + ); - if (!isRoomAdmin) { - debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "joinAnalyticsRoomsInSpace called by non-admin", + final List analyticsRoomIds = spaceHierarchy.rooms + .where((r) => r.roomType == PangeaRoomTypes.analytics) + .map((r) => r.roomId) + .toList(); + + await Future.wait( + analyticsRoomIds.map( + (roomID) => joinSpaceChild(roomID).catchError((err, s) { + debugPrint("Failed to join analytics room $roomID in space $id"); + ErrorHandler.logError( + e: err, + m: "Failed to join analytics room $roomID in space $id", + s: s, + ); + }), ), ); + } catch (err, s) { + ErrorHandler.logError( + e: err, + s: s, + ); return; } - - final spaceHierarchy = await client.getSpaceHierarchy( - id, - maxDepth: 1, - ); - - final List analyticsRoomIds = spaceHierarchy.rooms - .where( - (r) => r.roomType == PangeaRoomTypes.analytics, - ) - .map((r) => r.roomId) - .toList(); - - for (final String roomID in analyticsRoomIds) { - try { - await joinSpaceChild(roomID); - } catch (err, s) { - debugPrint("Failed to join analytics room $roomID in space $id"); - ErrorHandler.logError( - e: err, - m: "Failed to join analytics room $roomID in space $id", - s: s, - ); - } - } } // add 1 analytics room to 1 space @@ -84,107 +71,70 @@ extension AnalyticsRoomExtension on Room { } } - // Add analytics room to all spaces the user is a student in (1 analytics room to all spaces) - // So teachers can join them via space hierarchy - // Will not always work, as there may be spaces where students don't have permission to add chats - // But allows teachers to join analytics rooms without being invited - Future _addAnalyticsRoomToSpaces() async { - if (!isAnalyticsRoomOfUser(client.userID!)) { - debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "addAnalyticsRoomToSpaces called on non-analytics room", - ), - ); - return; - } - - for (final Room space in (await client.spaceImAStudentIn)) { - if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; - await space.addAnalyticsRoomToSpace(this); - } + /// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces). + /// Enables teachers to join student analytics rooms via space hierarchy. + /// Will not always work, as there may be spaces where students don't have permission to add chats, + /// but allows teachers to join analytics rooms without being invited. + void _addAnalyticsRoomToSpaces() { + if (!isAnalyticsRoomOfUser(client.userID!)) return; + Future.wait( + client.spacesImAStudentIn + .where((space) => !space.spaceChildren.any((sc) => sc.roomId == id)) + .map((space) => space.addAnalyticsRoomToSpace(this)), + ); } - // Add all analytics rooms to space - // Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space - Future _addAnalyticsRoomsToSpace() async { - await postLoad(); - final List allMyAnalyticsRooms = client.allMyAnalyticsRooms; - for (final Room analyticsRoom in allMyAnalyticsRooms) { - await addAnalyticsRoomToSpace(analyticsRoom); - } + /// Add all the user's analytics rooms to 1 space. + void _addAnalyticsRoomsToSpace() { + Future.wait( + client.allMyAnalyticsRooms.map((room) => addAnalyticsRoomToSpace(room)), + ); } - // invite teachers of 1 space to 1 analytics room + /// Invite teachers of 1 space to 1 analytics room Future _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { - if (!isSpace) { - debugPrint( - "inviteSpaceTeachersToAnalyticsRoom called on non-space room", - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "inviteSpaceTeachersToAnalyticsRoom called on non-space room", - ), - ); - return; - } + if (!isSpace) return; if (!analyticsRoom.participantListComplete) { await analyticsRoom.requestParticipants(); } + final List participants = analyticsRoom.getParticipants(); - for (final User teacher in (await teachers)) { - if (!participants.any((p) => p.id == teacher.id)) { - try { - await analyticsRoom.invite(teacher.id); - } catch (err, s) { - debugPrint( - "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", - ); + final List uninvitedTeachers = teachersLocal + .where((teacher) => !participants.contains(teacher)) + .toList(); + + Future.wait( + uninvitedTeachers.map( + (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) { ErrorHandler.logError( e: err, m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", s: s, ); - } - } - } + }), + ), + ); } - // Invite all teachers to 1 analytics room - // Handles case when students cannot add analytics room to space - // So teacher is still able to get analytics data for this student - Future _inviteTeachersToAnalyticsRoom() async { - if (client.userID == null) { - debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "inviteTeachersToAnalyticsRoom called with null userId", - ), - ); - return; - } - - if (!isAnalyticsRoomOfUser(client.userID!)) { - debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); - Sentry.addBreadcrumb( - Breadcrumb( - message: "inviteTeachersToAnalyticsRoom called on non-analytics room", - ), - ); - return; - } - - for (final Room space in (await client.spaceImAStudentIn)) { - await space.inviteSpaceTeachersToAnalyticsRoom(this); - } + /// Invite all the user's teachers to 1 analytics room. + /// Handles case when students cannot add analytics room to space + /// so teacher is still able to get analytics data for this student. + void _inviteTeachersToAnalyticsRoom() { + if (client.userID == null || !isAnalyticsRoomOfUser(client.userID!)) return; + Future.wait( + client.spacesImAStudentIn.map( + (space) => inviteSpaceTeachersToAnalyticsRoom(this), + ), + ); } - // Invite teachers of 1 space to all users' analytics rooms - Future _inviteSpaceTeachersToAnalyticsRooms() async { - for (final Room analyticsRoom in client.allMyAnalyticsRooms) { - await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); - } + /// Invite teachers of 1 space to all users' analytics rooms + void _inviteSpaceTeachersToAnalyticsRooms() { + Future.wait( + client.allMyAnalyticsRooms.map( + (room) => inviteSpaceTeachersToAnalyticsRoom(room), + ), + ); } Future _getLastAnalyticsEvent( diff --git a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart index 5799631b1..6354de96e 100644 --- a/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/space_settings_extension.dart @@ -55,27 +55,39 @@ extension SpaceRoomExtension on Room { : participants; } + /// Synchronous version of _teachers. Does not request participants, so this list may not be complete. + List get _teachersLocal { + if (!isSpace) return []; + return getParticipants() + .where( + (e) => + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList(); + } + + /// If the user is an admin of this space, and the space's + /// m.space.child power level hasn't yet been set, so it to 0 Future _setClassPowerLevels() async { try { - if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { - return; - } + if (!isRoomAdmin) return; final dynamic currentPower = getState(EventTypes.RoomPowerLevels); - if (currentPower is! Event?) { - return; - } - final Map? currentPowerContent = + if (currentPower is! Event?) return; + + final currentPowerContent = currentPower?.content["events"] as Map?; final spaceChildPower = currentPowerContent?[EventTypes.SpaceChild]; if (spaceChildPower == null && currentPowerContent != null) { - currentPowerContent["events"][EventTypes.SpaceChild] = 0; + currentPowerContent[EventTypes.SpaceChild] = 0; + currentPower!.content["events"] = currentPowerContent; await client.setRoomStateWithKey( id, EventTypes.RoomPowerLevels, - currentPower?.stateKey ?? "", - currentPowerContent, + currentPower.stateKey ?? "", + currentPower.content, ); } } catch (err, s) { diff --git a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart index 2db1acb4c..50a8cb7c2 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart @@ -64,7 +64,6 @@ class SpaceAnalyticsV2Controller extends State { Future getChatAndStudents() async { try { - await spaceRoom?.postLoad(); await spaceRoom?.requestParticipants(); if (spaceRoom != null) { diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index e06c6d0ba..007a04d93 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -49,15 +49,8 @@ class StudentAnalyticsController extends State { return _chats; } - List _spaces = []; - List get spaces { - if (_spaces.isEmpty) { - _pangeaController.matrixState.client.spaceImAStudentIn.then((result) { - setState(() => _spaces = result); - }); - } - return _spaces; - } + List get spaces => + _pangeaController.matrixState.client.spacesImAStudentIn; String? get userId { final id = _pangeaController.matrixState.client.userID; diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 9b950e15b..f6605d03f 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -36,7 +36,6 @@ void chatListHandleSpaceTap( if (await space.leaveIfFull()) { throw L10n.of(context)!.roomFull; } - await space.postLoad(); setActiveSpaceAndCloseChat(); }, onError: (exception) { @@ -72,7 +71,7 @@ void chatListHandleSpaceTap( throw L10n.of(context)!.roomFull; } if (space.isSpace) { - await space.joinAnalyticsRoomsInSpace(); + space.joinAnalyticsRoomsInSpace(); } setActiveSpaceAndCloseChat(); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 6058eecf9..808dfd9fc 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -111,12 +111,12 @@ abstract class ClientManager { // To make room emotes work 'im.ponies.room_emotes', // #Pangea - PangeaEventTypes.languageSettings, + // The things in this list will be loaded in the first sync, without having + // to postLoad to confirm that these state events are completely loaded PangeaEventTypes.rules, PangeaEventTypes.botOptions, - EventTypes.RoomTopic, - EventTypes.RoomAvatar, PangeaEventTypes.capacity, + EventTypes.RoomPowerLevels, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, From 798d9363157238b9bda0694af8e6dc8028f58fe2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 14:56:18 -0400 Subject: [PATCH 3/4] update space view when space children change --- lib/pages/chat_list/chat_list.dart | 14 +++++ lib/pages/chat_list/space_view.dart | 92 +++++++++++++---------------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 803b773e1..664876a42 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -476,6 +476,8 @@ class ChatListController extends State StreamSubscription? classStream; StreamSubscription? _invitedSpaceSubscription; StreamSubscription? _subscriptionStatusStream; + StreamSubscription? _spaceChildSubscription; + final Set hasUpdates = {}; //Pangea# @override @@ -567,6 +569,17 @@ class ChatListController extends State showSubscribedSnackbar(context); } }); + + _spaceChildSubscription ??= + pangeaController.matrixState.client.onRoomState.stream + .where( + (update) => + update.state.type == EventTypes.SpaceChild && + update.roomId != activeSpaceId, + ) + .listen((update) { + hasUpdates.add(update.roomId); + }); //Pangea# super.initState(); @@ -581,6 +594,7 @@ class ChatListController extends State classStream?.cancel(); _invitedSpaceSubscription?.cancel(); _subscriptionStatusStream?.cancel(); + _spaceChildSubscription?.cancel(); //Pangea# scrollController.removeListener(_onScroll); super.dispose(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 27cd48616..8c1407b5a 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -46,8 +45,8 @@ class _SpaceViewState extends State { Object? error; bool loading = false; // #Pangea - StreamSubscription? _roomSubscription; bool refreshing = false; + StreamSubscription? _roomSubscription; final String _chatCountsKey = 'chatCounts'; Map get chatCounts => Map.from( @@ -58,9 +57,24 @@ class _SpaceViewState extends State { @override void initState() { - loadHierarchy(); // #Pangea + // loadHierarchy(); + final bool hasUpdate = widget.controller.hasUpdates.contains( + widget.controller.activeSpaceId, + ); + loadHierarchy(hasUpdate: hasUpdate).then( + (_) => widget.controller.hasUpdates.remove( + widget.controller.activeSpaceId, + ), + ); loadChatCounts(); + _roomSubscription ??= + Matrix.of(context).client.onRoomState.stream.where((update) { + return update.state.type == EventTypes.SpaceChild && + update.roomId == widget.controller.activeSpaceId; + }).listen((update) { + loadHierarchy(hasUpdate: true); + }); // Pangea# super.initState(); } @@ -76,11 +90,11 @@ class _SpaceViewState extends State { void _refresh() { // #Pangea // _lastResponse.remove(widget.controller.activseSpaceId); - if (mounted) { - // Pangea# - loadHierarchy(); - // #Pangea - } + // loadHierarchy(); + if (mounted) setState(() => refreshing = true); + loadHierarchy(hasUpdate: true).whenComplete(() { + if (mounted) setState(() => refreshing = false); + }); // Pangea# } @@ -131,6 +145,7 @@ class _SpaceViewState extends State { /// message if an error occurs. Future loadHierarchy({ String? spaceId, + bool hasUpdate = false, }) async { if ((widget.controller.activeSpaceId == null && spaceId == null) || loading) { @@ -142,7 +157,7 @@ class _SpaceViewState extends State { setState(() {}); try { - await _loadHierarchy(spaceId: spaceId); + await _loadHierarchy(spaceId: spaceId, hasUpdate: hasUpdate); } catch (e, s) { if (mounted) { setState(() => error = e); @@ -159,6 +174,7 @@ class _SpaceViewState extends State { /// the active space id (or specified spaceId). Future _loadHierarchy({ String? spaceId, + bool hasUpdate = false, }) async { final client = Matrix.of(context).client; final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; @@ -177,7 +193,7 @@ class _SpaceViewState extends State { await activeSpace.postLoad(); // The current number of rooms loaded for this space that are visible in the UI - final int prevLength = _lastResponse[activeSpaceId] != null + final int prevLength = _lastResponse[activeSpaceId] != null && !hasUpdate ? filterHierarchyResponse( activeSpace, _lastResponse[activeSpaceId]!.rooms, @@ -187,6 +203,9 @@ class _SpaceViewState extends State { // Failsafe to prevent too many calls to the server in a row int callsToServer = 0; + GetSpaceHierarchyResponse? currentHierarchy = + hasUpdate ? null : _lastResponse[activeSpaceId]; + // Makes repeated calls to the server until 10 new visible rooms have // been loaded, or there are no rooms left to load. Using a loop here, // rather than one single call to the endpoint, because some spaces have @@ -195,16 +214,15 @@ class _SpaceViewState extends State { // coming through from those calls are analytics rooms). while (callsToServer < 5) { // if this space has been loaded and there are no more rooms to load, break - if (_lastResponse[activeSpaceId] != null && - _lastResponse[activeSpaceId]!.nextBatch == null) { + if (currentHierarchy != null && currentHierarchy.nextBatch == null) { break; } // if this space has been loaded and 10 new rooms have been loaded, break - if (_lastResponse[activeSpaceId] != null) { + if (currentHierarchy != null) { final int currentLength = filterHierarchyResponse( activeSpace, - _lastResponse[activeSpaceId]!.rooms, + currentHierarchy.rooms, ).length; if (currentLength - prevLength >= 10) { @@ -216,22 +234,26 @@ class _SpaceViewState extends State { final response = await client.getSpaceHierarchy( activeSpaceId, maxDepth: 1, - from: _lastResponse[activeSpaceId]?.nextBatch, + from: currentHierarchy?.nextBatch, limit: 100, ); callsToServer++; // if rooms have earlier been loaded for this space, add those // previously loaded rooms to the front of the response list - if (_lastResponse[activeSpaceId] != null) { + if (currentHierarchy != null) { response.rooms.insertAll( 0, - _lastResponse[activeSpaceId]?.rooms ?? [], + currentHierarchy.rooms, ); } // finally, set the response to the last response for this space - _lastResponse[activeSpaceId] = response; + currentHierarchy = response; + } + + if (currentHierarchy != null) { + _lastResponse[activeSpaceId] = currentHierarchy; } // After making those calls to the server, set the chat count for @@ -560,34 +582,6 @@ class _SpaceViewState extends State { } } - void refreshOnUpdate(SyncUpdate event) { - /* refresh on leave, invite, and space child update - not join events, because there's already a listener on - onTapSpaceChild, and they interfere with each other */ - if (widget.controller.activeSpaceId == null || !mounted || refreshing) { - return; - } - setState(() => refreshing = true); - final client = Matrix.of(context).client; - if (mounted && - event.isMembershipUpdateByType( - Membership.leave, - client.userID!, - ) || - event.isMembershipUpdateByType( - Membership.invite, - client.userID!, - ) || - event.isSpaceChildUpdate( - widget.controller.activeSpaceId!, - )) { - debugPrint("refresh on update"); - loadHierarchy().whenComplete(() { - if (mounted) setState(() => refreshing = false); - }); - } - } - bool includeSpaceChild( Room space, SpaceRoomsChunk hierarchyMember, @@ -769,12 +763,6 @@ class _SpaceViewState extends State { ); } - // #Pangea - _roomSubscription ??= client.onSync.stream - .where((event) => event.hasRoomUpdate) - .listen(refreshOnUpdate); - // Pangea# - final parentSpace = allSpaces.firstWhereOrNull( (space) => space.spaceChildren.any((child) => child.roomId == activeSpaceId), From 3a874902d3647d475ab312c9ef8baf1feaad5828 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Jul 2024 15:03:12 -0400 Subject: [PATCH 4/4] added some comments with justification for changes to how space view is auto loaded --- lib/pages/chat_list/chat_list.dart | 15 +++++++-------- lib/pages/chat_list/space_view.dart | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 664876a42..696178a72 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -570,14 +570,13 @@ class ChatListController extends State } }); - _spaceChildSubscription ??= - pangeaController.matrixState.client.onRoomState.stream - .where( - (update) => - update.state.type == EventTypes.SpaceChild && - update.roomId != activeSpaceId, - ) - .listen((update) { + // listen for space child updates for any space that is not the active space + // so that when the user navigates to the space that was updated, it will + // reload any rooms that have been added / removed + final client = pangeaController.matrixState.client; + _spaceChildSubscription ??= client.onRoomState.stream.where((u) { + return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId; + }).listen((update) { hasUpdates.add(update.roomId); }); //Pangea# diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 8c1407b5a..bdd19338d 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -59,19 +59,28 @@ class _SpaceViewState extends State { void initState() { // #Pangea // loadHierarchy(); + + // If, on launch, this room has had updates to its children, + // ensure the hierarchy is properly reloaded final bool hasUpdate = widget.controller.hasUpdates.contains( widget.controller.activeSpaceId, ); + loadHierarchy(hasUpdate: hasUpdate).then( + // remove this space ID from the set of space IDs with updates (_) => widget.controller.hasUpdates.remove( widget.controller.activeSpaceId, ), ); + loadChatCounts(); - _roomSubscription ??= - Matrix.of(context).client.onRoomState.stream.where((update) { - return update.state.type == EventTypes.SpaceChild && - update.roomId == widget.controller.activeSpaceId; + + // Listen for changes to the activeSpace's hierarchy, + // and reload the hierarchy when they come through + final client = Matrix.of(context).client; + _roomSubscription ??= client.onRoomState.stream.where((u) { + return u.state.type == EventTypes.SpaceChild && + u.roomId == widget.controller.activeSpaceId; }).listen((update) { loadHierarchy(hasUpdate: true); }); @@ -143,6 +152,7 @@ class _SpaceViewState extends State { /// spaceId, it will try to load the next batch and add the new rooms to the /// already loaded ones. Displays a loading indicator while loading, and an error /// message if an error occurs. + /// If hasUpdate is true, it will force the hierarchy to be reloaded. Future loadHierarchy({ String? spaceId, bool hasUpdate = false,