Merge branch '481-use-forked-matrix-sdk' of https://github.com/pangeachat/client into 481-use-forked-matrix-sdk

pull/1384/head
WilsonLe 1 year ago
commit 584dd421b6

@ -436,7 +436,9 @@ class ChatDetailsView extends StatelessWidget {
onTap: () =>
context.go('/rooms/${room.id}/invite'),
),
if (room.showClassEditOptions && room.isSpace)
if (room.showClassEditOptions &&
room.isSpace &&
!room.isSubspace)
SpaceDetailsToggleAddStudentsTile(
controller: controller,
),

@ -908,13 +908,14 @@ class ChatListController extends State<ChatList>
// #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",

@ -12,6 +12,7 @@ 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';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
@ -83,92 +84,167 @@ class _SpaceViewState extends State<SpaceView> {
// Pangea#
}
Future<GetSpaceHierarchyResponse> loadHierarchy([
String? prevBatch,
// #Pangea
// #Pangea
// Future<GetSpaceHierarchyResponse?> loadHierarchy([String? prevBatch]) async {
// final activeSpaceId = widget.controller.activeSpaceId;
// if (activeSpaceId == null) return null;
// final client = Matrix.of(context).client;
// final activeSpace = client.getRoomById(activeSpaceId);
// await activeSpace?.postLoad();
// setState(() {
// error = null;
// loading = true;
// });
// try {
// final response = await client.getSpaceHierarchy(
// activeSpaceId,
// maxDepth: 1,
// from: prevBatch,
// );
// if (prevBatch != null) {
// response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
// }
// setState(() {
// _lastResponse[activeSpaceId] = response;
// });
// return _lastResponse[activeSpaceId]!;
// } catch (e) {
// setState(() {
// error = e;
// });
// rethrow;
// } finally {
// setState(() {
// loading = false;
// });
// }
// }
/// Loads the hierarchy of the active space (or the given spaceId) and stores
/// it in _lastResponse map. If there's already a response in that map for the
/// 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.
Future<void> loadHierarchy({
String? spaceId,
// Pangea#
]) async {
// #Pangea
}) async {
if ((widget.controller.activeSpaceId == null && spaceId == null) ||
loading) {
return GetSpaceHierarchyResponse(
rooms: [],
nextBatch: null,
);
return;
}
setState(() {
error = null;
loading = true;
});
// Pangea#
loading = true;
error = null;
setState(() {});
// #Pangea
// final activeSpaceId = widget.controller.activeSpaceId!;
final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!;
// Pangea#
final client = Matrix.of(context).client;
try {
await _loadHierarchy(spaceId: spaceId);
} catch (e, s) {
if (mounted) {
setState(() => error = e);
}
ErrorHandler.logError(e: e, s: s);
} finally {
if (mounted) {
setState(() => loading = false);
}
}
}
/// Internal logic of loadHierarchy. It will load the hierarchy of
/// the active space id (or specified spaceId).
Future<void> _loadHierarchy({
String? spaceId,
}) async {
final client = Matrix.of(context).client;
final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!;
final activeSpace = client.getRoomById(activeSpaceId);
await activeSpace?.postLoad();
// #Pangea
// setState(() {
// error = null;
// loading = true;
// });
// Pangea#
if (activeSpace == null) {
ErrorHandler.logError(
e: Exception('Space not found in loadHierarchy'),
data: {'spaceId': activeSpaceId},
);
return;
}
try {
// Load all of the space's state events. Space Child events
// are used to filtering out unsuggested, unjoined rooms.
await activeSpace.postLoad();
// The current number of rooms loaded for this space that are visible in the UI
final int prevLength = _lastResponse[activeSpaceId] != null
? filterHierarchyResponse(
activeSpace,
_lastResponse[activeSpaceId]!.rooms,
).length
: 0;
// Failsafe to prevent too many calls to the server in a row
int callsToServer = 0;
// 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
// so many invisible rooms (analytics rooms) that it might look like
// pressing the 'load more' button does nothing (Because the only rooms
// 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) {
break;
}
// if this space has been loaded and 10 new rooms have been loaded, break
if (_lastResponse[activeSpaceId] != null) {
final int currentLength = filterHierarchyResponse(
activeSpace,
_lastResponse[activeSpaceId]!.rooms,
).length;
if (currentLength - prevLength >= 10) {
break;
}
}
// make the call to the server
final response = await client.getSpaceHierarchy(
activeSpaceId,
maxDepth: 1,
from: prevBatch,
// #Pangea
from: _lastResponse[activeSpaceId]?.nextBatch,
limit: 100,
// Pangea#
);
callsToServer++;
if (prevBatch != null) {
response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
}
// #Pangea
if (mounted) {
// Pangea#
setState(() {
_lastResponse[activeSpaceId] = response;
});
}
return _lastResponse[activeSpaceId]!;
} catch (e) {
// #Pangea
if (mounted) {
// Pangea#
setState(() {
error = e;
});
}
rethrow;
} finally {
// #Pangea
if (activeSpace != null) {
setChatCount(
activeSpace,
_lastResponse[activeSpaceId] ??
GetSpaceHierarchyResponse(
rooms: [],
),
// 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) {
response.rooms.insertAll(
0,
_lastResponse[activeSpaceId]?.rooms ?? [],
);
}
if (mounted) {
// Pangea#
setState(() {
loading = false;
});
}
// finally, set the response to the last response for this space
_lastResponse[activeSpaceId] = response;
}
// After making those calls to the server, set the chat count for
// this space. Used for the UI of the 'All Spaces' view
setChatCount(
activeSpace,
_lastResponse[activeSpaceId] ??
GetSpaceHierarchyResponse(
rooms: [],
),
);
}
// Pangea#
void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async {
final client = Matrix.of(context).client;
@ -479,12 +555,12 @@ class _SpaceViewState extends State<SpaceView> {
// if it's visible, and it hasn't been loaded yet, load chat count
if (isRootSpace && !chatCounts.containsKey(space.id)) {
await loadHierarchy(null, space.id);
loadHierarchy(spaceId: space.id);
}
}
}
Future<void> refreshOnUpdate(SyncUpdate event) async {
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 */
@ -506,44 +582,46 @@ class _SpaceViewState extends State<SpaceView> {
widget.controller.activeSpaceId!,
)) {
debugPrint("refresh on update");
await loadHierarchy();
loadHierarchy().whenComplete(() {
if (mounted) setState(() => refreshing = false);
});
}
setState(() => refreshing = false);
}
bool includeSpaceChild(sc, matchingSpaceChildren) {
bool includeSpaceChild(
Room space,
SpaceRoomsChunk hierarchyMember,
) {
if (!mounted) return false;
final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics;
final bool isMember = [Membership.join, Membership.invite]
.contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership);
final bool isSuggested = matchingSpaceChildren.any(
(matchingSpaceChild) =>
matchingSpaceChild.roomId == sc.roomId &&
matchingSpaceChild.suggested == true,
final bool isAnalyticsRoom =
hierarchyMember.roomType == PangeaRoomTypes.analytics;
final bool isMember = [Membership.join, Membership.invite].contains(
Matrix.of(context).client.getRoomById(hierarchyMember.roomId)?.membership,
);
final bool isSuggested =
space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true;
return !isAnalyticsRoom && (isMember || isSuggested);
}
List<SpaceRoomsChunk> filterSpaceChildren(
List<SpaceRoomsChunk> filterHierarchyResponse(
Room space,
List<SpaceRoomsChunk> spaceChildren,
List<SpaceRoomsChunk> hierarchyResponse,
) {
final childIds =
spaceChildren.map((hierarchyMember) => hierarchyMember.roomId);
final matchingSpaceChildren = space.spaceChildren
.where((spaceChild) => childIds.contains(spaceChild.roomId))
.toList();
final List<SpaceRoomsChunk> filteredChildren = [];
for (final child in hierarchyResponse) {
final isDuplicate = filteredChildren.any(
(filtered) => filtered.roomId == child.roomId,
);
if (isDuplicate) continue;
final filteredSpaceChildren = spaceChildren
.where(
(sc) => includeSpaceChild(
sc,
matchingSpaceChildren,
),
)
.toList();
return filteredSpaceChildren;
if (includeSpaceChild(space, child)) {
filteredChildren.add(child);
}
}
return filteredChildren;
}
int sortSpaceChildren(
@ -567,7 +645,7 @@ class _SpaceViewState extends State<SpaceView> {
) async {
final Map<String, int> updatedChatCounts = Map.from(chatCounts);
final List<SpaceRoomsChunk> spaceChildren = response?.rooms ?? [];
final filteredChildren = filterSpaceChildren(space, spaceChildren)
final filteredChildren = filterHierarchyResponse(space, spaceChildren)
.where((sc) => sc.roomId != space.id)
.toList();
updatedChatCounts[space.id] = filteredChildren.length;
@ -799,7 +877,7 @@ class _SpaceViewState extends State<SpaceView> {
final space =
Matrix.of(context).client.getRoomById(activeSpaceId);
if (space != null) {
spaceChildren = filterSpaceChildren(space, spaceChildren);
spaceChildren = filterHierarchyResponse(space, spaceChildren);
}
spaceChildren.sort(sortSpaceChildren);
// Pangea#
@ -818,7 +896,10 @@ class _SpaceViewState extends State<SpaceView> {
onPressed: loading
? null
: () {
loadHierarchy(response.nextBatch);
// #Pangea
// loadHierarchy(response.nextBatch);
loadHierarchy();
// Pangea#
},
),
);

@ -173,15 +173,17 @@ class NewSpaceController extends State<NewSpace> {
addToSpaceKey.currentState!.parent,
)
: null,
// initialState: [
// if (avatar != null)
// sdk.StateEvent(
// type: sdk.EventTypes.RoomAvatar,
// content: {'url': avatarUrl.toString()},
// ),
// ],
initialState: initialState,
// Pangea#
initialState: [
if (avatar != null)
sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()},
),
// #Pangea
...initialState,
// Pangea#
],
);
// #Pangea
final List<Future<dynamic>> futures = [

@ -31,18 +31,16 @@ class ClassController extends BaseController {
setState(data: {"activeSpaceId": classId});
}
Future<void> fixClassPowerLevels() async {
try {
final teacherSpaces =
await _pangeaController.matrixState.client.spacesImTeaching;
final List<Future<void>> classFixes = List<Room>.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<void> 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) {

@ -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<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
Room space,
) async {
await space.postLoad();
await space.requestParticipants();
final List<ConstructAnalyticsEvent> constructEvents = [];
for (final student in space.students) {
@ -788,7 +784,6 @@ class AnalyticsController extends BaseController {
);
return [];
}
await space.postLoad();
}
DateTime? lastUpdated;

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

@ -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<Room> _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<Room> _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<Room> 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<void> _updateAnalyticsRoomVisibility() async {
final List<Future> 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<void> _addAnalyticsRoomsToAllSpaces() async {
final List<Future> 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<void> _inviteAllTeachersToAllAnalyticsRooms() async {
final List<Future> 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<void> _joinAnalyticsRoomsInAllSpaces() async {
final List<Future> 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<void> _joinInvitedAnalyticsRooms() async {
final List<Room> 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<void> _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<Map<String, DateTime?>> _allAnalyticsRoomsLastUpdated() async {

@ -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<Room> 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<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms;
@ -31,35 +35,24 @@ extension PangeaClient on Client {
Future<void> updateAnalyticsRoomVisibility() async =>
await _updateAnalyticsRoomVisibility();
Future<void> addAnalyticsRoomsToAllSpaces() async =>
await _addAnalyticsRoomsToAllSpaces();
Future<void> inviteAllTeachersToAllAnalyticsRooms() async =>
await _inviteAllTeachersToAllAnalyticsRooms();
Future<void> joinAnalyticsRoomsInAllSpaces() async =>
await _joinAnalyticsRoomsInAllSpaces();
Future<void> joinInvitedAnalyticsRooms() async =>
await _joinInvitedAnalyticsRooms();
Future<void> 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<Map<String, DateTime?>> allAnalyticsRoomsLastUpdated() async =>
await _allAnalyticsRoomsLastUpdated();
// 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 spaceImAStudentIn async => await _spacesImStudyingIn;
List<Room> get spacesImAStudentIn => _spacesImStudyingIn;
List<Room> get spacesImIn => _spacesImIn;
Future<PangeaRoomRules?> get lastUpdatedRoomRules async =>
await _lastUpdatedRoomRules;
PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules;
// general_info

@ -1,23 +1,8 @@
part of "client_extension.dart";
extension SpaceClientExtension on Client {
Future<List<Room>> 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<Room> get _spacesImTeaching =>
rooms.where((e) => e.isSpace && e.isRoomAdmin).toList();
Future<List<Room>> get _chatsImAStudentIn async {
final List<String> nowteacherRoomIds = await teacherRoomIds;
@ -31,39 +16,18 @@ extension SpaceClientExtension on Client {
.toList();
}
Future<List<Room>> get _spacesImStudyingIn async {
final List<Room> 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<Room> get _spacesImStudyingIn =>
rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList();
List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
Future<PangeaRoomRules?> 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)

@ -1,6 +1,8 @@
part of "pangea_room_extension.dart";
extension ChildrenAndParentsRoomExtension on Room {
bool get _isSubspace => _pangeaSpaceParents.isNotEmpty;
//note this only will return rooms that the user has joined or been invited to
List<Room> get _joinedChildren {
if (!isSpace) return [];
@ -91,7 +93,7 @@ extension ChildrenAndParentsRoomExtension on Room {
String _nameIncludingParents(BuildContext context) {
String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
Room currentRoom = this;
if (currentRoom.pangeaSpaceParents.isEmpty) {
if (!currentRoom._isSubspace) {
return nameSoFar;
}
currentRoom = currentRoom.pangeaSpaceParents.first;
@ -100,7 +102,7 @@ extension ChildrenAndParentsRoomExtension on Room {
nameToAdd =
nameToAdd.length <= 10 ? nameToAdd : "${nameToAdd.substring(0, 10)}...";
nameSoFar = '$nameToAdd > $nameSoFar';
if (currentRoom.pangeaSpaceParents.isEmpty) {
if (!currentRoom._isSubspace) {
return nameSoFar;
}
return "... > $nameSoFar";
@ -161,4 +163,14 @@ extension ChildrenAndParentsRoomExtension on Room {
await setSpaceChild(roomId, suggested: suggested);
}
}
/// A map of child suggestion status for a space.
Map<String, bool> get _spaceChildSuggestionStatus {
if (!isSpace) return {};
final Map<String, bool> suggestionStatus = {};
for (final child in spaceChildren) {
suggestionStatus[child.roomId!] = child.suggested ?? true;
}
return suggestionStatus;
}
}

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

@ -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<void> joinAnalyticsRoomsInSpace() async =>
await _joinAnalyticsRoomsInSpace();
Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async =>
await _addAnalyticsRoomToSpace(analyticsRoom);
Future<void> 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<void> 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<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async =>
await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
Future<void> 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<void> inviteSpaceTeachersToAnalyticsRooms() async =>
await _inviteSpaceTeachersToAnalyticsRooms();
/// Invite teachers of 1 space to all users' analytics rooms
void inviteSpaceTeachersToAnalyticsRooms() =>
_inviteSpaceTeachersToAnalyticsRooms();
Future<AnalyticsEvent?> getLastAnalyticsEvent(
String type,
@ -122,6 +131,19 @@ extension PangeaRoom on Room {
}) async =>
await _pangeaSetSpaceChild(roomId, suggested: suggested);
/// Returns a map of child suggestion status for a space.
///
/// If the current object is not a space, an empty map is returned.
/// Otherwise, it iterates through each child in the `spaceChildren` list
/// and adds their suggestion status to the `suggestionStatus` map.
/// The suggestion status is determined by the `suggested` property of each child.
/// If the `suggested` property is `null`, it defaults to `true`.
Map<String, bool> get spaceChildSuggestionStatus =>
_spaceChildSuggestionStatus;
/// Checks if this space has a parent space
bool get isSubspace => _isSubspace;
// class_and_exchange_settings
DateTime? get rulesUpdatedAt => _rulesUpdatedAt;
@ -134,6 +156,12 @@ extension PangeaRoom on Room {
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();
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;

@ -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<void> _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<String> 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<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
@ -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<void> _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<void> _addAnalyticsRoomsToSpace() async {
await postLoad();
final List<Room> 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<void> _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<User> 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<User> 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<void> _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<void> _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<AnalyticsEvent?> _getLastAnalyticsEvent(

@ -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<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 {
try {
if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) {
return;
}
if (!isRoomAdmin) return;
final dynamic currentPower = getState(EventTypes.RoomPowerLevels);
if (currentPower is! Event?) {
return;
}
final Map<String, dynamic>? currentPowerContent =
if (currentPower is! Event?) return;
final currentPowerContent =
currentPower?.content["events"] as Map<String, dynamic>?;
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) {

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

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

@ -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(

@ -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,

Loading…
Cancel
Save