Merge pull request #497 from pangeachat/remove-postloads

reduced the number of calls to postLoad and the number of awaits for …
pull/1384/head
ggurdin 1 year ago committed by GitHub
commit 9cb6faf43c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -908,13 +908,14 @@ class ChatListController extends State<ChatList>
// #Pangea // #Pangea
if (mounted) { if (mounted) {
// TODO try not to await so much
GoogleAnalytics.analyticsUserUpdate(client.userID); GoogleAnalytics.analyticsUserUpdate(client.userID);
await pangeaController.subscriptionController.initialize(); await pangeaController.subscriptionController.initialize();
await pangeaController.myAnalytics.initialize(); await pangeaController.myAnalytics.initialize();
pangeaController.afterSyncAndFirstLoginInitialization(context); pangeaController.afterSyncAndFirstLoginInitialization(context);
await pangeaController.inviteBotToExistingSpaces(); await pangeaController.inviteBotToExistingSpaces();
await pangeaController.setPangeaPushRules(); await pangeaController.setPangeaPushRules();
await client.migrateAnalyticsRooms(); client.migrateAnalyticsRooms();
} else { } else {
ErrorHandler.logError( ErrorHandler.logError(
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",

@ -31,18 +31,16 @@ class ClassController extends BaseController {
setState(data: {"activeSpaceId": classId}); setState(data: {"activeSpaceId": classId});
} }
Future<void> fixClassPowerLevels() async { /// For all the spaces that the user is teaching, set the power levels
try { /// to enable all other users to add child rooms to the space.
final teacherSpaces = void fixClassPowerLevels() {
await _pangeaController.matrixState.client.spacesImTeaching; Future.wait(
final List<Future<void>> classFixes = List<Room>.from(teacherSpaces) _pangeaController.matrixState.client.spacesImTeaching.map(
.map((adminSpace) => adminSpace.setClassPowerLevels()) (space) => space.setClassPowerLevels().catchError((err, s) {
.toList(); ErrorHandler.logError(e: err, s: s);
await Future.wait(classFixes); }),
} catch (err, stack) { ),
debugger(when: kDebugMode); );
ErrorHandler.logError(e: err, s: stack);
}
} }
Future<void> checkForClassCodeAndSubscription(BuildContext context) async { Future<void> checkForClassCodeAndSubscription(BuildContext context) async {
@ -131,10 +129,10 @@ class ClassController extends BaseController {
_pangeaController.matrixState.client.getRoomById(classChunk.roomId); _pangeaController.matrixState.client.getRoomById(classChunk.roomId);
// when possible, add user's analytics room the to space they joined // 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 // and invite the space's teachers to the user's analytics rooms
await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); joinedSpace?.inviteSpaceTeachersToAnalyticsRooms();
GoogleAnalytics.joinClass(classCode); GoogleAnalytics.joinClass(classCode);
return; return;
} catch (err) { } catch (err) {

@ -198,9 +198,7 @@ class AnalyticsController extends BaseController {
// gets all the summary analytics events for the students // gets all the summary analytics events for the students
// in a space since the current timespace's cut off date // in a space since the current timespace's cut off date
// ensure that all the space's events are loaded (mainly the for langCode) // ensure that the participants of the space are loaded
// and that the participants are loaded
await space.postLoad();
await space.requestParticipants(); await space.requestParticipants();
// TODO switch to using list of futures // TODO switch to using list of futures
@ -439,7 +437,6 @@ class AnalyticsController extends BaseController {
timeSpan: currentAnalyticsTimeSpan, timeSpan: currentAnalyticsTimeSpan,
); );
} }
await space.postLoad();
} }
DateTime? lastUpdated; DateTime? lastUpdated;
@ -545,7 +542,6 @@ class AnalyticsController extends BaseController {
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs( Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
Room space, Room space,
) async { ) async {
await space.postLoad();
await space.requestParticipants(); await space.requestParticipants();
final List<ConstructAnalyticsEvent> constructEvents = []; final List<ConstructAnalyticsEvent> constructEvents = [];
for (final student in space.students) { for (final student in space.students) {
@ -788,7 +784,6 @@ class AnalyticsController extends BaseController {
); );
return []; return [];
} }
await space.postLoad();
} }
DateTime? lastUpdated; DateTime? lastUpdated;

@ -81,8 +81,7 @@ class PangeaController {
BuildContext context, BuildContext context,
) async { ) async {
await classController.checkForClassCodeAndSubscription(context); await classController.checkForClassCodeAndSubscription(context);
// startChatWithBotIfNotPresent(); classController.fixClassPowerLevels();
await classController.fixClassPowerLevels();
} }
/// Initialize controllers /// Initialize controllers

@ -1,48 +1,40 @@
part of "client_extension.dart"; part of "client_extension.dart";
extension AnalyticsClientExtension on Client { extension AnalyticsClientExtension on Client {
// get analytics room matching targetlanguage /// Get the logged in user's analytics room matching
// if not present, create it and invite teachers of that language /// a given langCode. If not present, create it.
// set description to let people know what the hell it is
Future<Room> _getMyAnalyticsRoom(String langCode) async { Future<Room> _getMyAnalyticsRoom(String langCode) async {
await roomsLoading; final Room? analyticsRoom = _analyticsRoomLocal(langCode);
// 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);
if (analyticsRoom != null) return analyticsRoom; if (analyticsRoom != null) return analyticsRoom;
return _makeAnalyticsRoom(langCode); return _makeAnalyticsRoom(langCode);
} }
//note: if langCode is null and user has >1 analyticsRooms then this could /// Get local analytics room for a given langCode and
//return the wrong one. this is to account for when an exchange might not /// optional userId (if not specified, uses current user).
//be in a class. /// If user is invited to the room, joins the room.
Room? _analyticsRoomLocal(String? langCode, [String? userIdParam]) { Room? _analyticsRoomLocal(String langCode, [String? userIdParam]) {
final Room? analyticsRoom = rooms.firstWhereOrNull((e) { final Room? analyticsRoom = rooms.firstWhereOrNull((e) {
return e.isAnalyticsRoom && return e.isAnalyticsRoom &&
e.isAnalyticsRoomOfUser(userIdParam ?? userID!) && e.isAnalyticsRoomOfUser(userIdParam ?? userID!) &&
(langCode != null ? e.isMadeForLang(langCode) : true); e.isMadeForLang(langCode);
}); });
if (analyticsRoom != null && if (analyticsRoom != null &&
analyticsRoom.membership == Membership.invite) { analyticsRoom.membership == Membership.invite) {
debugger(when: kDebugMode); debugger(when: kDebugMode);
analyticsRoom analyticsRoom.join().onError(
.join()
.onError(
(error, stackTrace) => (error, stackTrace) =>
ErrorHandler.logError(e: error, s: stackTrace), ErrorHandler.logError(e: error, s: stackTrace),
) );
.then((value) => analyticsRoom.postLoad());
return analyticsRoom; return analyticsRoom;
} }
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<Room> _makeAnalyticsRoom(String langCode) async { Future<Room> _makeAnalyticsRoom(String langCode) async {
final String roomID = await createRoom( final String roomID = await createRoom(
creationContent: { creationContent: {
@ -53,7 +45,6 @@ extension AnalyticsClientExtension on Client {
topic: "This room stores learning analytics for $userID.", topic: "This room stores learning analytics for $userID.",
invite: [ invite: [
...(await myTeachers).map((e) => e.id), ...(await myTeachers).map((e) => e.id),
// BotName.localBot,
BotName.byEnvironment, BotName.byEnvironment,
], ],
); );
@ -66,14 +57,14 @@ extension AnalyticsClientExtension on Client {
// add this analytics room to all spaces so teachers can join them // add this analytics room to all spaces so teachers can join them
// via the space hierarchy // via the space hierarchy
await analyticsRoom?.addAnalyticsRoomToSpaces(); analyticsRoom?.addAnalyticsRoomToSpaces();
// and invite all teachers to new analytics room // and invite all teachers to new analytics room
await analyticsRoom?.inviteTeachersToAnalyticsRoom(); analyticsRoom?.inviteTeachersToAnalyticsRoom();
return getRoomById(roomID)!; return getRoomById(roomID)!;
} }
// Get all my analytics rooms /// Get all my analytics rooms
List<Room> get _allMyAnalyticsRooms => rooms List<Room> get _allMyAnalyticsRooms => rooms
.where( .where(
(e) => e.isAnalyticsRoomOfUser(userID!), (e) => e.isAnalyticsRoomOfUser(userID!),
@ -83,76 +74,77 @@ extension AnalyticsClientExtension on Client {
// migration function to change analytics rooms' vsibility to public // migration function to change analytics rooms' vsibility to public
// so they will appear in the space hierarchy // so they will appear in the space hierarchy
Future<void> _updateAnalyticsRoomVisibility() async { Future<void> _updateAnalyticsRoomVisibility() async {
final List<Future> makePublicFutures = []; await Future.wait(
for (final Room room in allMyAnalyticsRooms) { allMyAnalyticsRooms.map((room) async {
final visability = await getRoomVisibilityOnDirectory(room.id); final visability = await getRoomVisibilityOnDirectory(room.id);
if (visability != Visibility.public) { if (visability != Visibility.public) {
await setRoomVisibilityOnDirectory( await setRoomVisibilityOnDirectory(
room.id, room.id,
visibility: Visibility.public, visibility: Visibility.public,
); );
} }
} }),
await Future.wait(makePublicFutures); );
} }
// Add all the users' analytics room to all the spaces the student studies in /// Add all the users' analytics room to all the spaces the user is studying in
// So teachers can join them via space hierarchy /// 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 /// Allows teachers to join analytics rooms without being invited.
// But allows teachers to join analytics rooms without being invited void _addAnalyticsRoomsToAllSpaces() {
Future<void> _addAnalyticsRoomsToAllSpaces() async {
final List<Future> addFutures = [];
for (final Room room in allMyAnalyticsRooms) { for (final Room room in allMyAnalyticsRooms) {
addFutures.add(room.addAnalyticsRoomToSpaces()); room.addAnalyticsRoomToSpaces();
} }
await Future.wait(addFutures);
} }
// Invite teachers to all my analytics room /// Invite teachers to all my analytics room.
// Handles case when students cannot add analytics room to space(s) /// Handles case when students cannot add analytics room to space(s)
// So teacher is still able to get analytics data for this student /// so teacher is still able to get analytics data for this student
Future<void> _inviteAllTeachersToAllAnalyticsRooms() async { void _inviteAllTeachersToAllAnalyticsRooms() {
final List<Future> inviteFutures = []; for (final Room room in allMyAnalyticsRooms) {
for (final Room analyticsRoom in allMyAnalyticsRooms) { room.inviteTeachersToAnalyticsRoom();
inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom());
} }
await Future.wait(inviteFutures);
} }
// Join all analytics rooms in all spaces // Join all analytics rooms in all spaces
// Allows teachers to join analytics rooms without being invited // Allows teachers to join analytics rooms without being invited
Future<void> _joinAnalyticsRoomsInAllSpaces() async { Future<void> _joinAnalyticsRoomsInAllSpaces() async {
final List<Future> joinFutures = []; for (final Room space in _spacesImTeaching) {
for (final Room space in (await _spacesImTeaching)) { // Each call to joinAnalyticsRoomsInSpace calls getSpaceHierarchy, which has a
joinFutures.add(space.joinAnalyticsRoomsInSpace()); // 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 /// Join invited analytics rooms.
// Checks for invites to any student analytics rooms /// Checks for invites to any student analytics rooms.
// Handles case of analytics rooms that can't be added to some space(s) /// Handles case of analytics rooms that can't be added to some space(s).
Future<void> _joinInvitedAnalyticsRooms() async { void _joinInvitedAnalyticsRooms() {
final List<Room> allRooms = List.from(rooms); Future.wait(
for (final Room room in allRooms) { rooms
if (room.membership == Membership.invite && room.isAnalyticsRoom) { .where(
try { (room) =>
await room.join(); room.membership == Membership.invite && room.isAnalyticsRoom,
} catch (err) { )
debugPrint("Failed to join analytics room ${room.id}"); .map(
} (room) => room.join().catchError((err, s) {
} ErrorHandler.logError(e: err, s: s);
} }),
),
);
} }
// helper function to join all relevant analytics rooms /// Helper function to join all relevant analytics rooms
// and set up those rooms to be joined by relevant teachers /// and set up those rooms to be joined by other users.
Future<void> _migrateAnalyticsRooms() async { void _migrateAnalyticsRooms() {
await _updateAnalyticsRoomVisibility(); _updateAnalyticsRoomVisibility().then((_) {
await _addAnalyticsRoomsToAllSpaces(); _addAnalyticsRoomsToAllSpaces();
await _inviteAllTeachersToAllAnalyticsRooms(); _inviteAllTeachersToAllAnalyticsRooms();
await _joinInvitedAnalyticsRooms(); _joinInvitedAnalyticsRooms();
await _joinAnalyticsRoomsInAllSpaces(); _joinAnalyticsRoomsInAllSpaces();
});
} }
Future<Map<String, DateTime?>> _allAnalyticsRoomsLastUpdated() async { Future<Map<String, DateTime?>> _allAnalyticsRoomsLastUpdated() async {

@ -1,7 +1,6 @@
import 'dart:developer'; import 'dart:developer';
import 'package:collection/collection.dart'; 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/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
@ -20,10 +19,15 @@ part "space_extension.dart";
extension PangeaClient on Client { extension PangeaClient on Client {
// analytics // analytics
/// Get the logged in user's analytics room matching
/// a given langCode. If not present, create it.
Future<Room> getMyAnalyticsRoom(String langCode) async => Future<Room> getMyAnalyticsRoom(String langCode) async =>
await _getMyAnalyticsRoom(langCode); 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); _analyticsRoomLocal(langCode, userIdParam);
List<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms; List<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms;
@ -31,35 +35,24 @@ extension PangeaClient on Client {
Future<void> updateAnalyticsRoomVisibility() async => Future<void> updateAnalyticsRoomVisibility() async =>
await _updateAnalyticsRoomVisibility(); await _updateAnalyticsRoomVisibility();
Future<void> addAnalyticsRoomsToAllSpaces() async => /// Helper function to join all relevant analytics rooms
await _addAnalyticsRoomsToAllSpaces(); /// and set up those rooms to be joined by other users.
void migrateAnalyticsRooms() => _migrateAnalyticsRooms();
Future<void> inviteAllTeachersToAllAnalyticsRooms() async =>
await _inviteAllTeachersToAllAnalyticsRooms();
Future<void> joinAnalyticsRoomsInAllSpaces() async =>
await _joinAnalyticsRoomsInAllSpaces();
Future<void> joinInvitedAnalyticsRooms() async =>
await _joinInvitedAnalyticsRooms();
Future<void> migrateAnalyticsRooms() async => await _migrateAnalyticsRooms();
Future<Map<String, DateTime?>> allAnalyticsRoomsLastUpdated() async => Future<Map<String, DateTime?>> allAnalyticsRoomsLastUpdated() async =>
await _allAnalyticsRoomsLastUpdated(); await _allAnalyticsRoomsLastUpdated();
// spaces // spaces
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching; List<Room> get spacesImTeaching => _spacesImTeaching;
Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn; Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn; List<Room> get spacesImAStudentIn => _spacesImStudyingIn;
List<Room> get spacesImIn => _spacesImIn; List<Room> get spacesImIn => _spacesImIn;
Future<PangeaRoomRules?> get lastUpdatedRoomRules async => PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules;
await _lastUpdatedRoomRules;
// general_info // general_info

@ -1,23 +1,8 @@
part of "client_extension.dart"; part of "client_extension.dart";
extension SpaceClientExtension on Client { extension SpaceClientExtension on Client {
Future<List<Room>> get _spacesImTeaching async { List<Room> get _spacesImTeaching =>
final allSpaces = rooms.where((room) => room.isSpace); rooms.where((e) => e.isSpace && e.isRoomAdmin).toList();
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;
}
Future<List<Room>> get _chatsImAStudentIn async { Future<List<Room>> get _chatsImAStudentIn async {
final List<String> nowteacherRoomIds = await teacherRoomIds; final List<String> nowteacherRoomIds = await teacherRoomIds;
@ -31,39 +16,18 @@ extension SpaceClientExtension on Client {
.toList(); .toList();
} }
Future<List<Room>> get _spacesImStudyingIn async { List<Room> get _spacesImStudyingIn =>
final List<Room> joinedSpaces = rooms rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList();
.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<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList(); List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
Future<PangeaRoomRules?> get _lastUpdatedRoomRules async => PangeaRoomRules? get _lastUpdatedRoomRules => _spacesImTeaching
(await _spacesImTeaching) .where((space) => space.rulesUpdatedAt != null)
.where((space) => space.rulesUpdatedAt != null) .sorted(
.sorted( (a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!), )
) .firstOrNull
.firstOrNull ?.pangeaRoomRules;
?.pangeaRoomRules;
// LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms // LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms
// .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null) // .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null)

@ -2,7 +2,6 @@ part of "pangea_room_extension.dart";
extension EventsRoomExtension on Room { extension EventsRoomExtension on Room {
Future<bool> _leaveIfFull() async { Future<bool> _leaveIfFull() async {
await postLoad();
if (!isRoomAdmin && if (!isRoomAdmin &&
(_capacity != null) && (_capacity != null) &&
(await _numNonAdmins) > (_capacity!)) { (await _numNonAdmins) > (_capacity!)) {

@ -49,26 +49,35 @@ part "user_permissions_extension.dart";
extension PangeaRoom on Room { extension PangeaRoom on Room {
// analytics // analytics
/// Join analytics rooms in space.
/// Allows teachers to join analytics rooms without being invited.
Future<void> joinAnalyticsRoomsInSpace() async => Future<void> joinAnalyticsRoomsInSpace() async =>
await _joinAnalyticsRoomsInSpace(); await _joinAnalyticsRoomsInSpace();
Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async => Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async =>
await _addAnalyticsRoomToSpace(analyticsRoom); await _addAnalyticsRoomToSpace(analyticsRoom);
Future<void> addAnalyticsRoomToSpaces() async => /// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces).
await _addAnalyticsRoomToSpaces(); /// 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<void> addAnalyticsRoomsToSpace() async => /// Add all the user's analytics rooms to 1 space.
await _addAnalyticsRoomsToSpace(); void addAnalyticsRoomsToSpace() => _addAnalyticsRoomsToSpace();
/// Invite teachers of 1 space to 1 analytics room
Future<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async => Future<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async =>
await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
Future<void> inviteTeachersToAnalyticsRoom() async => /// Invite all the user's teachers to 1 analytics room.
await _inviteTeachersToAnalyticsRoom(); /// 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<void> inviteSpaceTeachersToAnalyticsRooms() async => /// Invite teachers of 1 space to all users' analytics rooms
await _inviteSpaceTeachersToAnalyticsRooms(); void inviteSpaceTeachersToAnalyticsRooms() =>
_inviteSpaceTeachersToAnalyticsRooms();
Future<AnalyticsEvent?> getLastAnalyticsEvent( Future<AnalyticsEvent?> getLastAnalyticsEvent(
String type, String type,
@ -147,6 +156,12 @@ extension PangeaRoom on Room {
Future<List<User>> get teachers async => await _teachers; Future<List<User>> get teachers async => await _teachers;
/// Synchronous version of teachers getter. Does not request
/// participants, so this list may not be complete.
List<User> 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<void> setClassPowerLevels() async => await _setClassPowerLevels(); Future<void> setClassPowerLevels() async => await _setClassPowerLevels();
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent; Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;

@ -1,58 +1,45 @@
part of "pangea_room_extension.dart"; part of "pangea_room_extension.dart";
extension AnalyticsRoomExtension on Room { extension AnalyticsRoomExtension on Room {
// Join analytics rooms in space /// Join analytics rooms in space.
// Allows teachers to join analytics rooms without being invited /// Allows teachers to join analytics rooms without being invited.
Future<void> _joinAnalyticsRoomsInSpace() async { Future<void> _joinAnalyticsRoomsInSpace() async {
if (!isSpace) { try {
debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); if (!isSpace) {
Sentry.addBreadcrumb( debugger(when: kDebugMode);
Breadcrumb( return;
message: "joinAnalyticsRoomsInSpace called on non-space room", }
),
);
return;
}
// added delay because without it power levels don't load and user is not if (!isRoomAdmin) return;
// recognized as admin final spaceHierarchy = await client.getSpaceHierarchy(
await Future.delayed(const Duration(milliseconds: 500)); id,
await postLoad(); maxDepth: 1,
);
if (!isRoomAdmin) { final List<String> analyticsRoomIds = spaceHierarchy.rooms
debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); .where((r) => r.roomType == PangeaRoomTypes.analytics)
Sentry.addBreadcrumb( .map((r) => r.roomId)
Breadcrumb( .toList();
message: "joinAnalyticsRoomsInSpace called by non-admin",
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; return;
} }
final spaceHierarchy = await client.getSpaceHierarchy(
id,
maxDepth: 1,
);
final List<String> 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 // 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) /// 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 /// 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 /// 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 /// but allows teachers to join analytics rooms without being invited.
Future<void> _addAnalyticsRoomToSpaces() async { void _addAnalyticsRoomToSpaces() {
if (!isAnalyticsRoomOfUser(client.userID!)) { if (!isAnalyticsRoomOfUser(client.userID!)) return;
debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); Future.wait(
Sentry.addBreadcrumb( client.spacesImAStudentIn
Breadcrumb( .where((space) => !space.spaceChildren.any((sc) => sc.roomId == id))
message: "addAnalyticsRoomToSpaces called on non-analytics room", .map((space) => space.addAnalyticsRoomToSpace(this)),
), );
);
return;
}
for (final Room space in (await client.spaceImAStudentIn)) {
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
await space.addAnalyticsRoomToSpace(this);
}
} }
// Add all analytics rooms to space /// Add all the user's analytics rooms to 1 space.
// Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space void _addAnalyticsRoomsToSpace() {
Future<void> _addAnalyticsRoomsToSpace() async { Future.wait(
await postLoad(); client.allMyAnalyticsRooms.map((room) => addAnalyticsRoomToSpace(room)),
final List<Room> allMyAnalyticsRooms = client.allMyAnalyticsRooms; );
for (final Room analyticsRoom in allMyAnalyticsRooms) {
await addAnalyticsRoomToSpace(analyticsRoom);
}
} }
// invite teachers of 1 space to 1 analytics room /// Invite teachers of 1 space to 1 analytics room
Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async {
if (!isSpace) { if (!isSpace) return;
debugPrint(
"inviteSpaceTeachersToAnalyticsRoom called on non-space room",
);
Sentry.addBreadcrumb(
Breadcrumb(
message:
"inviteSpaceTeachersToAnalyticsRoom called on non-space room",
),
);
return;
}
if (!analyticsRoom.participantListComplete) { if (!analyticsRoom.participantListComplete) {
await analyticsRoom.requestParticipants(); await analyticsRoom.requestParticipants();
} }
final List<User> participants = analyticsRoom.getParticipants(); final List<User> participants = analyticsRoom.getParticipants();
for (final User teacher in (await teachers)) { final List<User> uninvitedTeachers = teachersLocal
if (!participants.any((p) => p.id == teacher.id)) { .where((teacher) => !participants.contains(teacher))
try { .toList();
await analyticsRoom.invite(teacher.id);
} catch (err, s) { Future.wait(
debugPrint( uninvitedTeachers.map(
"Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", (teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
);
ErrorHandler.logError( ErrorHandler.logError(
e: err, e: err,
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
s: s, s: s,
); );
} }),
} ),
} );
} }
// Invite all teachers to 1 analytics room /// Invite all the user's teachers to 1 analytics room.
// Handles case when students cannot add analytics room to space /// Handles case when students cannot add analytics room to space
// So teacher is still able to get analytics data for this student /// so teacher is still able to get analytics data for this student.
Future<void> _inviteTeachersToAnalyticsRoom() async { void _inviteTeachersToAnalyticsRoom() {
if (client.userID == null) { if (client.userID == null || !isAnalyticsRoomOfUser(client.userID!)) return;
debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); Future.wait(
Sentry.addBreadcrumb( client.spacesImAStudentIn.map(
Breadcrumb( (space) => inviteSpaceTeachersToAnalyticsRoom(this),
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 teachers of 1 space to all users' analytics rooms /// Invite teachers of 1 space to all users' analytics rooms
Future<void> _inviteSpaceTeachersToAnalyticsRooms() async { void _inviteSpaceTeachersToAnalyticsRooms() {
for (final Room analyticsRoom in client.allMyAnalyticsRooms) { Future.wait(
await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); client.allMyAnalyticsRooms.map(
} (room) => inviteSpaceTeachersToAnalyticsRoom(room),
),
);
} }
Future<AnalyticsEvent?> _getLastAnalyticsEvent( Future<AnalyticsEvent?> _getLastAnalyticsEvent(

@ -55,27 +55,39 @@ extension SpaceRoomExtension on Room {
: participants; : participants;
} }
/// Synchronous version of _teachers. Does not request participants, so this list may not be complete.
List<User> 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<void> _setClassPowerLevels() async { Future<void> _setClassPowerLevels() async {
try { try {
if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { if (!isRoomAdmin) return;
return;
}
final dynamic currentPower = getState(EventTypes.RoomPowerLevels); final dynamic currentPower = getState(EventTypes.RoomPowerLevels);
if (currentPower is! Event?) { if (currentPower is! Event?) return;
return;
} final currentPowerContent =
final Map<String, dynamic>? currentPowerContent =
currentPower?.content["events"] as Map<String, dynamic>?; currentPower?.content["events"] as Map<String, dynamic>?;
final spaceChildPower = currentPowerContent?[EventTypes.SpaceChild]; final spaceChildPower = currentPowerContent?[EventTypes.SpaceChild];
if (spaceChildPower == null && currentPowerContent != null) { if (spaceChildPower == null && currentPowerContent != null) {
currentPowerContent["events"][EventTypes.SpaceChild] = 0; currentPowerContent[EventTypes.SpaceChild] = 0;
currentPower!.content["events"] = currentPowerContent;
await client.setRoomStateWithKey( await client.setRoomStateWithKey(
id, id,
EventTypes.RoomPowerLevels, EventTypes.RoomPowerLevels,
currentPower?.stateKey ?? "", currentPower.stateKey ?? "",
currentPowerContent, currentPower.content,
); );
} }
} catch (err, s) { } catch (err, s) {

@ -64,7 +64,6 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
Future<void> getChatAndStudents() async { Future<void> getChatAndStudents() async {
try { try {
await spaceRoom?.postLoad();
await spaceRoom?.requestParticipants(); await spaceRoom?.requestParticipants();
if (spaceRoom != null) { if (spaceRoom != null) {

@ -49,15 +49,8 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
return _chats; return _chats;
} }
List<Room> _spaces = []; List<Room> get spaces =>
List<Room> get spaces { _pangeaController.matrixState.client.spacesImAStudentIn;
if (_spaces.isEmpty) {
_pangeaController.matrixState.client.spaceImAStudentIn.then((result) {
setState(() => _spaces = result);
});
}
return _spaces;
}
String? get userId { String? get userId {
final id = _pangeaController.matrixState.client.userID; final id = _pangeaController.matrixState.client.userID;

@ -36,7 +36,6 @@ void chatListHandleSpaceTap(
if (await space.leaveIfFull()) { if (await space.leaveIfFull()) {
throw L10n.of(context)!.roomFull; throw L10n.of(context)!.roomFull;
} }
await space.postLoad();
setActiveSpaceAndCloseChat(); setActiveSpaceAndCloseChat();
}, },
onError: (exception) { onError: (exception) {
@ -72,7 +71,7 @@ void chatListHandleSpaceTap(
throw L10n.of(context)!.roomFull; throw L10n.of(context)!.roomFull;
} }
if (space.isSpace) { if (space.isSpace) {
await space.joinAnalyticsRoomsInSpace(); space.joinAnalyticsRoomsInSpace();
} }
setActiveSpaceAndCloseChat(); setActiveSpaceAndCloseChat();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

@ -111,12 +111,12 @@ abstract class ClientManager {
// To make room emotes work // To make room emotes work
'im.ponies.room_emotes', 'im.ponies.room_emotes',
// #Pangea // #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.rules,
PangeaEventTypes.botOptions, PangeaEventTypes.botOptions,
EventTypes.RoomTopic,
EventTypes.RoomAvatar,
PangeaEventTypes.capacity, PangeaEventTypes.capacity,
EventTypes.RoomPowerLevels,
// Pangea# // Pangea#
}, },
logLevel: kReleaseMode ? Level.warning : Level.verbose, logLevel: kReleaseMode ? Level.warning : Level.verbose,

Loading…
Cancel
Save