1389 space analytics download updates (#1395)

* feat: invite other admins to analytics rooms

* fix: properly invite space admins to analytics rooms

* feat: simplify process for getting space admins into analytics rooms

* feat: add columns for over and under 80% correct use for original written and system generated uses
pull/1593/head
ggurdin 10 months ago committed by GitHub
parent 771bd4b6c3
commit 8cbe1ea1f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4668,7 +4668,7 @@
"numLemmasUsedCorrectly": "Number of lemmas used correctly at least once",
"listLemmasUsedCorrectly": "List of lemmas used correctly at least once",
"numLemmasUsedIncorrectly": "Number of lemmas used incorrectly at least once",
"listLemmasUsedIncorrectly": "List of lemmas used incorrectly at least once",
"listLemmasUsedIncorrectly": "Number of lemmas used correctly 0 times",
"numLemmasSmallXP": "Number of lemmas with 0 - 30 XP",
"numLemmasMediumXP": "Number of lemmas with 31 - 200 XP",
"numLemmasLargeXP": "Number of lemmas with > 200 XP",
@ -4677,8 +4677,10 @@
"listLemmasLargeXP": "List of lemmas with > 200 XP",
"numGrammarConcepts": "Number of grammar concepts",
"listGrammarConcepts": "List of grammar concepts",
"listGrammarConceptsUsedCorrectly": "List of grammar concepts used correctly at least 80% of the time",
"listGrammarConceptsUsedIncorrectly": "List of grammar concepts used correctly less than 80% of the time",
"listGrammarConceptsUsedCorrectly": "List of grammar concepts used correctly in original messages at least 80% of the time",
"listGrammarConceptsUsedIncorrectly": "List of grammar concepts used correctly in original messages less than 80% of the time",
"listGrammarConceptsUseCorrectlySystemGenerated": "List of grammar concepts chosen correctly from system-generated suggestions at least 80% of the time",
"listGrammarConceptsUseIncorrectlySystemGenerated": "List of grammar concepts chosen correctly from system-generated suggestions less than 80% of the time",
"incorrectGrammarConceptsUseCases": "Use cases of grammar concepts used incorrectly",
"listGrammarConceptsSmallXP": "List of grammar concepts with 0 - 30 XP",
"listGrammarConceptsMediumXP": "List of grammar concepts with 31 - 200 XP",
@ -4696,5 +4698,10 @@
"analyticsNotAvailable": "User analytics not available",
"downloading": "Downloading...",
"failedFetchUserAnalytics": "Failed to download user analytics",
"downloadComplete": "Download complete!"
}
"downloadComplete": "Download complete!",
"dataAvailable": "Data availability",
"lemmasNeverUsedCorrectly": "Number of lemmas used correctly 0 times",
"available": "Available",
"unavailable": "Unavailable",
"accessingMemberAnalytics": "Accessing member analytics..."
}

@ -20,7 +20,6 @@ import 'package:fluffychat/pages/chat/send_file_dialog.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/app_version_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
@ -1122,7 +1121,6 @@ class ChatListController extends State<ChatList>
// #Pangea
void _initPangeaControllers(Client client) {
GoogleAnalytics.analyticsUserUpdate(client.userID);
client.migrateAnalyticsRooms();
MatrixState.pangeaController.initControllers();
if (mounted) {
MatrixState.pangeaController.classController.joinCachedSpaceCode(context);

@ -12,7 +12,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
@ -36,17 +35,19 @@ class ClassController extends BaseController {
/// 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,
data: {
"spaceID": space.id,
},
);
}),
),
_pangeaController.matrixState.client.rooms
.where((room) => room.isSpace && room.isRoomAdmin)
.map(
(space) => space.setClassPowerLevels().catchError((err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {
"spaceID": space.id,
},
);
}),
),
);
}
@ -148,11 +149,6 @@ class ClassController extends BaseController {
return;
}
// when possible, add user's analytics room the to space they joined
room.addAnalyticsRoomsToSpace();
// and invite the space's teachers to the user's analytics rooms
room.inviteSpaceTeachersToAnalyticsRooms();
GoogleAnalytics.joinClass(classCode);
if (room.client.getRoomById(room.id)?.membership != Membership.join) {

@ -28,6 +28,7 @@ class GetAnalyticsController {
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
StreamController<AnalyticsStreamUpdate> analyticsStream =
StreamController.broadcast();
StreamSubscription? _joinSpaceSubscription;
ConstructListModel constructListModel = ConstructListModel(uses: []);
Completer<void> initCompleter = Completer<void>();
@ -72,10 +73,19 @@ class GetAnalyticsController {
if (initCompleter.isCompleted) return;
try {
_client.updateAnalyticsRoomVisibility();
_client.addAnalyticsRoomsToSpaces();
_analyticsUpdateSubscription ??= _pangeaController
.putAnalytics.analyticsUpdateStream.stream
.listen(_onAnalyticsUpdate);
// When a newly-joined space comes through in a sync
// update, add the analytics rooms to the space
_joinSpaceSubscription ??= _client.onSync.stream
.where(_client.isJoinSpaceSyncUpdate)
.listen((_) => _client.addAnalyticsRoomsToSpaces());
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
await _getConstructs();
constructListModel.updateConstructs([
@ -99,6 +109,8 @@ class GetAnalyticsController {
constructListModel.dispose();
_analyticsUpdateSubscription?.cancel();
_analyticsUpdateSubscription = null;
_joinSpaceSubscription?.cancel();
_joinSpaceSubscription = null;
initCompleter = Completer<void>();
_cache.clear();
// perMessage.dispose();

@ -2,6 +2,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
enum AnalyticsSummaryEnum {
username,
dataAvailable,
level,
totalXP,
numLemmas,
@ -19,8 +20,10 @@ enum AnalyticsSummaryEnum {
numMorphConstructs,
listMorphConstructs,
listMorphConstructsUsedCorrectly,
listMorphConstructsUsedIncorrectly,
listMorphConstructsUsedCorrectlyOriginal,
listMorphConstructsUsedIncorrectlyOriginal,
listMorphConstructsUsedCorrectlySystem,
listMorphConstructsUsedIncorrectlySystem,
// list morph 0 - 30 XP
listMorphSmallXP,
@ -45,6 +48,8 @@ extension AnalyticsSummaryEnumExtension on AnalyticsSummaryEnum {
switch (this) {
case AnalyticsSummaryEnum.username:
return l10n.username;
case AnalyticsSummaryEnum.dataAvailable:
return l10n.dataAvailable;
case AnalyticsSummaryEnum.level:
return l10n.level;
case AnalyticsSummaryEnum.totalXP:
@ -65,10 +70,14 @@ extension AnalyticsSummaryEnumExtension on AnalyticsSummaryEnum {
return l10n.numGrammarConcepts;
case AnalyticsSummaryEnum.listMorphConstructs:
return l10n.listGrammarConcepts;
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectly:
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlyOriginal:
return l10n.listGrammarConceptsUsedCorrectly;
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectly:
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlyOriginal:
return l10n.listGrammarConceptsUsedIncorrectly;
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlySystem:
return l10n.listGrammarConceptsUseCorrectlySystemGenerated;
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlySystem:
return l10n.listGrammarConceptsUseIncorrectlySystemGenerated;
case AnalyticsSummaryEnum.listMorphSmallXP:
return l10n.listGrammarConceptsSmallXP;
case AnalyticsSummaryEnum.listMorphMediumXP:

@ -53,24 +53,15 @@ extension AnalyticsClientExtension on Client {
},
name: "$userID $langCode Analytics",
topic: "This room stores learning analytics for $userID.",
invite: [
...(await myTeachers).map((e) => e.id),
BotName.byEnvironment,
],
preset: CreateRoomPreset.publicChat,
visibility: Visibility.private,
);
if (getRoomById(roomID) == null) {
// Wait for room actually appears in sync
await waitForRoomInSync(roomID, join: true);
}
final Room? analyticsRoom = getRoomById(roomID);
// add this analytics room to all spaces so teachers can join them
// via the space hierarchy
analyticsRoom?.addAnalyticsRoomToSpaces();
// and invite all teachers to new analytics room
analyticsRoom?.inviteTeachersToAnalyticsRoom();
_addAnalyticsRoomsToSpaces();
return getRoomById(roomID)!;
}
@ -81,97 +72,96 @@ extension AnalyticsClientExtension on Client {
)
.toList();
// migration function to change analytics rooms' vsibility to public
// so they will appear in the space hierarchy
/// Update the visibility of all analytics rooms to private (do they don't show in search
/// results) and set the join rules to public (so they come through in space hierarchy response)
Future<void> _updateAnalyticsRoomVisibility() async {
if (userID == null || userID == BotName.byEnvironment) return;
final Random random = Random();
for (final analyticsRoom in allMyAnalyticsRooms) {
if (userID == null) return;
final visibility = await getRoomVisibilityOnDirectory(analyticsRoom.id);
// if making a call to the server (either to update visibility or join rules)
// add a delay at the end of this interaction to prevent overloading the server
int delay = 0;
if (visibility != Visibility.private ||
analyticsRoom.joinRules != JoinRules.public) {
delay = random.nextInt(10);
}
final visibilityFutures = allMyAnalyticsRooms.map((room) async {
final visability = await getRoomVisibilityOnDirectory(room.id);
if (visability != Visibility.private) {
// don't show in search results
if (visibility != Visibility.private) {
await setRoomVisibilityOnDirectory(
room.id,
analyticsRoom.id,
visibility: Visibility.private,
);
}
}).toList();
final joinRulesFutures = allMyAnalyticsRooms.map((room) async {
if (room.joinRules != JoinRules.public) {
await room.setJoinRules(JoinRules.public);
// do show in space hierarchy
if (analyticsRoom.joinRules != JoinRules.public) {
await analyticsRoom.setJoinRules(JoinRules.public);
}
}).toList();
await Future.wait(
visibilityFutures + joinRulesFutures,
);
}
/// 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() {
if (userID == null || userID == BotName.byEnvironment) return;
for (final Room room in allMyAnalyticsRooms) {
room.addAnalyticsRoomToSpaces();
await Future.delayed(Duration(seconds: delay));
}
}
/// 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() {
/// Space admins join analytics rooms in spaces via the space hierarchy,
/// so other members of the space need to add their analytics rooms to the space.
Future<void> _addAnalyticsRoomsToSpaces() async {
if (userID == null || userID == BotName.byEnvironment) return;
for (final Room room in allMyAnalyticsRooms) {
room.inviteTeachersToAnalyticsRoom();
}
}
final spaces = rooms.where((room) => room.isSpace).toList();
final Random random = Random();
for (final space in spaces) {
if (userID == null) return;
final List<Room> roomsNotAdded = allMyAnalyticsRooms.where((room) {
return !space.spaceChildren.any((child) => child.roomId == room.id);
}).toList();
if (roomsNotAdded.isEmpty) continue;
for (final analyticsRoom in roomsNotAdded) {
if (userID == null) return;
try {
await space.setSpaceChild(analyticsRoom.id);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"spaceID": space.id,
"analyticsRoomID": analyticsRoom.id,
"userID": userID,
},
);
}
}
// Join all analytics rooms in all spaces
// Allows teachers to join analytics rooms without being invited
Future<void> _joinAnalyticsRoomsInAllSpaces() async {
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(),
// add a delay before checking the next space to prevent overloading the server
final delay = random.nextInt(10);
debugPrint(
"added ${roomsNotAdded.length} rooms to space ${space.id}, delay: $delay",
);
await Future.delayed(Duration(seconds: delay));
}
}
/// 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,
data: {
"roomID": room.id,
},
);
}),
),
);
}
/// 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();
});
/// Check if sync update includes newly joined room. Used by the
/// GetAnalyticsController to add analytics rooms to newly joined spaces.
bool _isJoinSpaceSyncUpdate(SyncUpdate update) {
if (update.rooms?.join == null) return false;
return update.rooms!.join!.values
.where(
(e) =>
e.state != null &&
e.state!.any(
(e) =>
e.type == EventTypes.RoomCreate &&
e.content['type'] == 'm.space',
),
)
.isNotEmpty;
}
}

@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
@ -9,13 +10,11 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/constants/model_keys.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/models/space_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
part "client_analytics_extension.dart";
part "general_info_extension.dart";
part "space_extension.dart";
extension PangeaClient on Client {
// analytics
@ -33,22 +32,18 @@ extension PangeaClient on Client {
List<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms;
/// Update the visibility of all analytics rooms to private (do they don't show in search
/// results) and set the join rules to public (so they come through in space hierarchy response)
Future<void> updateAnalyticsRoomVisibility() async =>
await _updateAnalyticsRoomVisibility();
_updateAnalyticsRoomVisibility();
/// Helper function to join all relevant analytics rooms
/// and set up those rooms to be joined by other users.
void migrateAnalyticsRooms() => _migrateAnalyticsRooms();
/// Space admins join analytics rooms in spaces via the space hierarchy,
/// so other members of the space need to add their analytics rooms to the space.
Future<void> addAnalyticsRoomsToSpaces() async =>
_addAnalyticsRoomsToSpaces();
// spaces
List<Room> get spacesImTeaching => _spacesImTeaching;
List<Room> get spacesImAStudentIn => _spacesImStudyingIn;
List<Room> get spacesImIn => _spacesImIn;
PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules;
bool isJoinSpaceSyncUpdate(SyncUpdate update) =>
_isJoinSpaceSyncUpdate(update);
// general_info

@ -3,7 +3,8 @@ part of "client_extension.dart";
extension GeneralInfoClientExtension on Client {
Future<List<User>> get _myTeachers async {
final List<User> teachers = [];
for (final classRoom in spacesImIn) {
final spaces = rooms.where((room) => room.isSpace);
for (final classRoom in spaces) {
for (final teacher in await classRoom.teachers) {
// If person requesting list of teachers is a teacher in another classroom, don't add them to the list
if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) {

@ -1,28 +0,0 @@
part of "client_extension.dart";
extension SpaceClientExtension on Client {
List<Room> get _spacesImTeaching =>
rooms.where((e) => e.isSpace && e.isRoomAdmin).toList();
List<Room> get _spacesImStudyingIn =>
rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList();
List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
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)
// .sorted(
// (a, b) => b.languageSettingsUpdatedAt!
// .compareTo(a.languageSettingsUpdatedAt!),
// )
// .firstOrNull
// ?.languageSettings;
}

@ -19,14 +19,11 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/constants/bot_mode.dart';
import 'package:fluffychat/pangea/constants/class_code_constants.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -38,7 +35,6 @@ import '../../../config/app_config.dart';
import '../../constants/pangea_event_types.dart';
import '../../models/choreo_record.dart';
import '../../models/representation_content_model.dart';
import '../client_extension/client_extension.dart';
part "room_analytics_extension.dart";
part "room_children_and_parents_extension.dart";
@ -51,35 +47,7 @@ part "room_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);
/// 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();
/// 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);
/// 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();
/// Invite teachers of 1 space to all users' analytics rooms
void inviteSpaceTeachersToAnalyticsRooms() =>
_inviteSpaceTeachersToAnalyticsRooms();
Future<void> joinAnalyticsRooms() async => await _joinAnalyticsRooms();
Future<DateTime?> analyticsLastUpdated(String userId) async {
return await _analyticsLastUpdated(userId);
@ -165,9 +133,6 @@ extension PangeaRoom on Room {
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
Future<List<LanguageModel>> targetLanguages() async =>
await _targetLanguages();
// events
Future<bool> leaveIfFull() async => await _leaveIfFull();

@ -1,156 +1,77 @@
part of "pangea_room_extension.dart";
extension AnalyticsRoomExtension on Room {
/// Join analytics rooms in space.
/// Allows teachers to join analytics rooms without being invited.
Future<void> _joinAnalyticsRoomsInSpace() async {
try {
if (!isSpace) {
debugger(when: kDebugMode);
return;
}
Future<List<SpaceRoomsChunk>> _getFullSpaceHierarchy() async {
final resp = await client.getSpaceHierarchy(
id,
limit: 100,
maxDepth: 1,
);
if (client.userID == null || !isRoomAdmin) return;
final spaceHierarchy = await client.getSpaceHierarchy(
final List<SpaceRoomsChunk> rooms = resp.rooms;
String? nextBatch = resp.nextBatch;
int tries = 0;
while (nextBatch != null && tries <= 5) {
final nextResp = await client.getSpaceHierarchy(
id,
from: nextBatch,
limit: 100,
maxDepth: 1,
);
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,
data: {
"roomID": roomID,
"spaceID": id,
},
);
}),
),
);
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {
"spaceID": id,
},
);
return;
}
}
// add 1 analytics room to 1 space
Future<void> _addAnalyticsRoomToSpace(Room analyticsRoom) async {
if (!isSpace) {
debugPrint("addAnalyticsRoomToSpace called on non-space room");
Sentry.addBreadcrumb(
Breadcrumb(
message: "addAnalyticsRoomToSpace called on non-space room",
),
);
return Future.value();
rooms.addAll(nextResp.rooms);
nextBatch = nextResp.nextBatch;
tries++;
}
// Checks that user has permission to add child to space
if (!canSendEvent(EventTypes.SpaceChild)) return;
if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return;
try {
await setSpaceChild(analyticsRoom.id);
} catch (err) {
debugPrint(
"Failed to add analytics room ${analyticsRoom.id} for student to space $id",
);
Sentry.addBreadcrumb(
Breadcrumb(
message: "Failed to add analytics room to space $id",
),
);
}
return rooms;
}
/// 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 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
Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async {
if (!isSpace) return;
if (!analyticsRoom.participantListComplete) {
await analyticsRoom.requestParticipants();
}
Future<void> _joinAnalyticsRooms() async {
final List<SpaceRoomsChunk> rooms = await _getFullSpaceHierarchy();
final unjoinedAnalyticsRooms = rooms.where(
(room) {
if (room.roomType != PangeaRoomTypes.analytics) return false;
final matchingRoom = client.rooms.firstWhereOrNull(
(r) => r.id == room.roomId,
);
return matchingRoom == null ||
matchingRoom.membership != Membership.join;
},
).toList();
const batchSize = 5;
int batchNum = 0;
while (batchSize * batchNum < unjoinedAnalyticsRooms.length) {
final batch =
unjoinedAnalyticsRooms.sublist(batchSize * batchNum).take(batchSize);
batchNum++;
for (final analyticsRoom in batch) {
try {
final syncFuture =
client.waitForRoomInSync(analyticsRoom.roomId, join: true);
await client.joinRoom(analyticsRoom.roomId);
await syncFuture;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"spaceID": id,
"roomID": analyticsRoom.roomId,
},
);
}
}
final List<User> participants = analyticsRoom.getParticipants();
final List<User> uninvitedTeachers = (await teachers)
.where((teacher) => !participants.contains(teacher))
.toList();
if (analyticsRoom.canSendEvent(EventTypes.RoomMember)) {
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,
data: {
"teacherID": teacher.id,
"analyticsRoomID": analyticsRoom.id,
},
);
}),
),
);
if (batchSize * batchNum < unjoinedAnalyticsRooms.length) {
await Future.delayed(const Duration(milliseconds: 7500));
}
}
}
/// 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
void _inviteSpaceTeachersToAnalyticsRooms() {
Future.wait(
client.allMyAnalyticsRooms.map(
(room) => inviteSpaceTeachersToAnalyticsRoom(room),
),
);
}
Future<DateTime?> _analyticsLastUpdated(String userId) async {
final List<Event> events =
await getRoomAnalyticsEvents(count: 1, userID: userId);

@ -110,35 +110,6 @@ extension SpaceRoomExtension on Room {
return null;
}
Future<List<LanguageModel>> _targetLanguages() async {
await requestParticipants();
final students = _students;
final Map<LanguageModel, int> langCounts = {};
final List<Room> allRooms = client.rooms;
for (final User student in students) {
for (final Room room in allRooms) {
if (!room.isAnalyticsRoomOfUser(student.id)) continue;
final String? langCode = room.madeForLang;
if (langCode == null ||
langCode.isEmpty ||
langCode == LanguageKeys.unknownLanguage) {
continue;
}
final LanguageModel? lang = PangeaLanguage.byLangCode(langCode);
if (lang == null) continue;
langCounts[lang] ??= 0;
langCounts[lang] = langCounts[lang]! + 1;
}
}
// get a list of language models, sorted
// by the number of students who are learning that language
return langCounts.entries.map((entry) => entry.key).toList()
..sort(
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
);
}
// DateTime? get _languageSettingsUpdatedAt {
// if (!isSpace) return null;
// return languageSettingsStateEvent?.originServerTs ?? creationTime;

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/enum/analytics/analytics_summary_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
@ -8,104 +10,150 @@ import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
class AnalyticsSummaryModel {
String username;
int level;
int totalXP;
bool dataAvailable;
int? level;
int? totalXP;
int numLemmas;
int numLemmasUsedCorrectly;
int numLemmasUsedIncorrectly;
int? numLemmas;
int? numLemmasUsedCorrectly;
int? numLemmasUsedIncorrectly;
/// 0 - 30 XP
int numLemmasSmallXP;
int? numLemmasSmallXP;
/// 31 - 200 XP
int numLemmasMediumXP;
int? numLemmasMediumXP;
/// > 200 XP
int numLemmasLargeXP;
int? numLemmasLargeXP;
int numMorphConstructs;
List<String> listMorphConstructs;
List<String> listMorphConstructsUsedCorrectly;
List<String> listMorphConstructsUsedIncorrectly;
int? numMorphConstructs;
List<String>? listMorphConstructs;
List<String>? listMorphConstructsUsedCorrectlyOriginal;
List<String>? listMorphConstructsUsedIncorrectlyOriginal;
List<String>? listMorphConstructsUsedCorrectlySystem;
List<String>? listMorphConstructsUsedIncorrectlySystem;
// list morph 0 - 30 XP
List<String> listMorphSmallXP;
List<String>? listMorphSmallXP;
// list morph 31 - 200 XP
List<String> listMorphMediumXP;
List<String>? listMorphMediumXP;
// list morph 200 - 500 XP
List<String> listMorphLargeXP;
List<String>? listMorphLargeXP;
// list morph > 500 XP
List<String> listMorphHugeXP;
List<String>? listMorphHugeXP;
int numMessagesSent;
int numWordsTyped;
int numChoicesCorrect;
int numChoicesIncorrect;
int? numMessagesSent;
int? numWordsTyped;
int? numChoicesCorrect;
int? numChoicesIncorrect;
AnalyticsSummaryModel({
required this.username,
required this.level,
required this.totalXP,
required this.numLemmas,
required this.numLemmasUsedCorrectly,
required this.numLemmasUsedIncorrectly,
required this.numLemmasSmallXP,
required this.numLemmasMediumXP,
required this.numLemmasLargeXP,
required this.numMorphConstructs,
required this.listMorphConstructs,
required this.listMorphConstructsUsedCorrectly,
required this.listMorphConstructsUsedIncorrectly,
required this.listMorphSmallXP,
required this.listMorphMediumXP,
required this.listMorphLargeXP,
required this.listMorphHugeXP,
required this.numMessagesSent,
required this.numWordsTyped,
required this.numChoicesCorrect,
required this.numChoicesIncorrect,
required this.dataAvailable,
this.level,
this.totalXP,
this.numLemmas,
this.numLemmasUsedCorrectly,
this.numLemmasUsedIncorrectly,
this.numLemmasSmallXP,
this.numLemmasMediumXP,
this.numLemmasLargeXP,
this.numMorphConstructs,
this.listMorphConstructs,
this.listMorphConstructsUsedCorrectlyOriginal,
this.listMorphConstructsUsedIncorrectlyOriginal,
this.listMorphConstructsUsedCorrectlySystem,
this.listMorphConstructsUsedIncorrectlySystem,
this.listMorphSmallXP,
this.listMorphMediumXP,
this.listMorphLargeXP,
this.listMorphHugeXP,
this.numMessagesSent,
this.numWordsTyped,
this.numChoicesCorrect,
this.numChoicesIncorrect,
});
static AnalyticsSummaryModel emptyModel(String userID) {
return AnalyticsSummaryModel(
username: userID,
dataAvailable: false,
);
}
static AnalyticsSummaryModel fromConstructListModel(
ConstructListModel model,
String userID,
ConstructListModel? model,
String Function(ConstructUses) getCopy,
BuildContext context,
) {
final vocabLemmas = LemmasToUsesWrapper(
model.lemmasToUses(type: ConstructTypeEnum.vocab),
);
final morphLemmas = LemmasToUsesWrapper(
model.lemmasToUses(type: ConstructTypeEnum.morph),
);
final vocabLemmas = model != null
? LemmasToUsesWrapper(
model.lemmasToUses(type: ConstructTypeEnum.vocab),
)
: null;
final morphLemmas = model != null
? LemmasToUsesWrapper(
model.lemmasToUses(type: ConstructTypeEnum.morph),
)
: null;
final morphLemmasPercentCorrect = morphLemmas.lemmasByPercent(
percent: 0.8,
getCopy: getCopy,
);
final List<String> correctOriginalUseLemmas = [];
final List<String> correctSystemUseLemmas = [];
final List<String> incorrectOriginalUseLemmas = [];
final List<String> incorrectSystemUseLemmas = [];
if (morphLemmas != null) {
final originalWrittenUses = morphLemmas.lemmasByPercent(
filter: (use) =>
use.useType == ConstructUseTypeEnum.wa ||
use.useType == ConstructUseTypeEnum.ga,
percent: 0.8,
);
correctSystemUseLemmas.addAll(originalWrittenUses.over);
incorrectSystemUseLemmas.addAll(originalWrittenUses.under);
final systemGeneratedUses = morphLemmas.lemmasByPercent(
filter: (use) =>
use.useType != ConstructUseTypeEnum.wa &&
use.useType != ConstructUseTypeEnum.ga &&
use.useType != ConstructUseTypeEnum.unk &&
use.pointValue != 0,
percent: 0.8,
);
correctSystemUseLemmas.addAll(systemGeneratedUses.over);
incorrectSystemUseLemmas.addAll(systemGeneratedUses.under);
}
final vocabLemmasCorrect = vocabLemmas?.lemmasByCorrectUse();
final vocabLemmasCorrect = vocabLemmas.lemmasByCorrectUse(getCopy: getCopy);
int numWordsTyped = 0;
int numChoicesCorrect = 0;
int numChoicesIncorrect = 0;
for (final use in model.uses) {
if (use.useType.summaryEnumType == AnalyticsSummaryEnum.numWordsTyped) {
numWordsTyped++;
} else if (use.useType.summaryEnumType ==
AnalyticsSummaryEnum.numChoicesCorrect) {
numChoicesCorrect++;
} else if (use.useType.summaryEnumType ==
AnalyticsSummaryEnum.numChoicesIncorrect) {
numChoicesIncorrect++;
int? numWordsTyped;
int? numChoicesCorrect;
int? numChoicesIncorrect;
if (model != null) {
numWordsTyped = 0;
numChoicesCorrect = 0;
numChoicesIncorrect = 0;
for (final use in model.uses) {
if (use.useType.summaryEnumType == AnalyticsSummaryEnum.numWordsTyped) {
numWordsTyped = numWordsTyped! + 1;
} else if (use.useType.summaryEnumType ==
AnalyticsSummaryEnum.numChoicesCorrect) {
numChoicesCorrect = numChoicesCorrect! + 1;
} else if (use.useType.summaryEnumType ==
AnalyticsSummaryEnum.numChoicesIncorrect) {
numChoicesIncorrect = numChoicesIncorrect! + 1;
}
}
}
final numMessageSent = model.uses
final numMessageSent = model?.uses
.where((use) => use.useType.sentByUser)
.map((use) => use.metadata.eventId)
.toSet()
@ -113,37 +161,41 @@ class AnalyticsSummaryModel {
return AnalyticsSummaryModel(
username: userID,
level: model.level,
totalXP: model.totalXP,
numLemmas: model.vocabLemmas,
numLemmasUsedCorrectly: vocabLemmasCorrect.over.length,
numLemmasUsedIncorrectly: vocabLemmasCorrect.under.length,
numLemmasSmallXP: vocabLemmas.thresholdedLemmas(start: 0, end: 30).length,
dataAvailable: model != null,
level: model?.level,
totalXP: model?.totalXP,
numLemmas: model?.vocabLemmas,
numLemmasUsedCorrectly: vocabLemmasCorrect?.over.length,
numLemmasUsedIncorrectly: vocabLemmasCorrect?.under.length,
numLemmasSmallXP:
vocabLemmas?.thresholdedLemmas(start: 0, end: 30).length,
numLemmasMediumXP:
vocabLemmas.thresholdedLemmas(start: 31, end: 200).length,
numLemmasLargeXP: vocabLemmas.thresholdedLemmas(start: 201).length,
numMorphConstructs: model.grammarLemmas,
listMorphConstructs: morphLemmas.lemmasToUses.entries
vocabLemmas?.thresholdedLemmas(start: 31, end: 200).length,
numLemmasLargeXP: vocabLemmas?.thresholdedLemmas(start: 201).length,
numMorphConstructs: model?.grammarLemmas,
listMorphConstructs: morphLemmas?.lemmasToUses.entries
.map((entry) => getCopy(entry.value.first))
.toList(),
listMorphConstructsUsedCorrectly: morphLemmasPercentCorrect.over,
listMorphConstructsUsedIncorrectly: morphLemmasPercentCorrect.under,
listMorphSmallXP: morphLemmas.thresholdedLemmas(
listMorphConstructsUsedCorrectlyOriginal: correctOriginalUseLemmas,
listMorphConstructsUsedIncorrectlyOriginal: incorrectOriginalUseLemmas,
listMorphConstructsUsedCorrectlySystem: correctSystemUseLemmas,
listMorphConstructsUsedIncorrectlySystem: incorrectSystemUseLemmas,
listMorphSmallXP: morphLemmas?.thresholdedLemmas(
start: 0,
end: 30,
getCopy: getCopy,
),
listMorphMediumXP: morphLemmas.thresholdedLemmas(
listMorphMediumXP: morphLemmas?.thresholdedLemmas(
start: 31,
end: 200,
getCopy: getCopy,
),
listMorphLargeXP: morphLemmas.thresholdedLemmas(
listMorphLargeXP: morphLemmas?.thresholdedLemmas(
start: 201,
end: 500,
getCopy: getCopy,
),
listMorphHugeXP: morphLemmas.thresholdedLemmas(
listMorphHugeXP: morphLemmas?.thresholdedLemmas(
start: 501,
getCopy: getCopy,
),
@ -154,10 +206,14 @@ class AnalyticsSummaryModel {
);
}
dynamic getValue(AnalyticsSummaryEnum key) {
dynamic getValue(AnalyticsSummaryEnum key, BuildContext context) {
switch (key) {
case AnalyticsSummaryEnum.username:
return username;
case AnalyticsSummaryEnum.dataAvailable:
return dataAvailable
? L10n.of(context).available
: L10n.of(context).unavailable;
case AnalyticsSummaryEnum.level:
return level;
case AnalyticsSummaryEnum.totalXP:
@ -178,10 +234,14 @@ class AnalyticsSummaryModel {
return numMorphConstructs;
case AnalyticsSummaryEnum.listMorphConstructs:
return listMorphConstructs;
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectly:
return listMorphConstructsUsedCorrectly;
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectly:
return listMorphConstructsUsedIncorrectly;
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlyOriginal:
return listMorphConstructsUsedCorrectlyOriginal;
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlyOriginal:
return listMorphConstructsUsedIncorrectlyOriginal;
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlySystem:
return listMorphConstructsUsedCorrectlySystem;
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlySystem:
return listMorphConstructsUsedIncorrectlySystem;
case AnalyticsSummaryEnum.listMorphSmallXP:
return listMorphSmallXP;
case AnalyticsSummaryEnum.listMorphMediumXP:
@ -214,8 +274,10 @@ class AnalyticsSummaryModel {
'numLemmasLargeXP': numLemmasLargeXP,
'numMorphConstructs': numMorphConstructs,
'listMorphConstructs': listMorphConstructs,
'listMorphConstructsUsedCorrectly': listMorphConstructsUsedCorrectly,
'listMorphConstructsUsedIncorrectly': listMorphConstructsUsedIncorrectly,
'listMorphConstructsUsedCorrectly':
listMorphConstructsUsedCorrectlyOriginal,
'listMorphConstructsUsedIncorrectly':
listMorphConstructsUsedIncorrectlyOriginal,
'listMorphSmallXP': listMorphSmallXP,
'listMorphMediumXP': listMorphMediumXP,
'listMorphLargeXP': listMorphLargeXP,

@ -247,8 +247,54 @@ class LemmasToUsesWrapper {
LemmasToUsesWrapper(this.lemmasToUses);
Map<String, List<OneConstructUse>> lemmasToFilteredUses(
bool Function(OneConstructUse) filter,
) {
final Map<String, List<OneConstructUse>> lemmasToOneConstructUses = {};
for (final entry in lemmasToUses.entries) {
final lemma = entry.key;
final uses = entry.value;
lemmasToOneConstructUses[lemma] =
uses.expand((use) => use.uses).toList().where(filter).toList();
}
return lemmasToOneConstructUses;
}
LemmasOverUnderList lemmasByPercent({
required bool Function(OneConstructUse) filter,
required double percent,
}) {
final List<String> correctUseLemmas = [];
final List<String> incorrectUseLemmas = [];
final uses = lemmasToFilteredUses(filter);
for (final entry in uses.entries) {
final List<OneConstructUse> correctUses = [];
final List<OneConstructUse> incorrectUses = [];
final lemma = entry.key;
final uses = entry.value.toList();
for (final use in uses) {
use.pointValue > 0 ? correctUses.add(use) : incorrectUses.add(use);
}
final totalUses = correctUses.length + incorrectUses.length;
final percent = totalUses == 0 ? 0 : correctUses.length / totalUses;
percent > 0.8
? correctUseLemmas.add(lemma)
: incorrectUseLemmas.add(lemma);
}
return LemmasOverUnderList(
over: correctUseLemmas,
under: incorrectUseLemmas,
);
}
/// Return an object containing two lists, one of lemmas with
/// any correct uses and one of lemmas with any incorrect uses
/// any correct uses and one of lemmas no correct uses
LemmasOverUnderList lemmasByCorrectUse({
String Function(ConstructUses)? getCopy,
}) {
@ -260,46 +306,13 @@ class LemmasToUsesWrapper {
final copy = getCopy?.call(constructUses.first) ?? lemma;
if (constructUses.any((use) => use.hasCorrectUse)) {
correctLemmas.add(copy);
}
if (constructUses.any((use) => use.hasIncorrectUse)) {
} else {
incorrectLemmas.add(copy);
}
}
return LemmasOverUnderList(over: correctLemmas, under: incorrectLemmas);
}
/// Return an object containing two lists, one of lemmas with percent used
/// correctly > percent and one of lemmas with percent used correctly < percent
LemmasOverUnderList lemmasByPercent({
double percent = 0.8,
String Function(ConstructUses)? getCopy,
}) {
final List<String> overLemmas = [];
final List<String> underLemmas = [];
for (final entry in lemmasToUses.entries) {
final lemma = entry.key;
final constructUses = entry.value;
final uses = constructUses.map((u) => u.uses).expand((e) => e).toList();
int correct = 0;
int incorrect = 0;
for (final use in uses) {
if (use.pointValue > 0) {
correct++;
} else if (use.pointValue < 0) {
incorrect++;
}
}
if (correct + incorrect == 0) continue;
final copy = getCopy?.call(constructUses.first) ?? lemma;
final percent = correct / (correct + incorrect);
percent >= percent ? overLemmas.add(copy) : underLemmas.add(copy);
}
return LemmasOverUnderList(over: overLemmas, under: underLemmas);
}
int totalXP(String lemma) {
final uses = lemmasToUses[lemma];
if (uses == null) return 0;

@ -1,168 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ClassInvitationSelection extends StatefulWidget {
const ClassInvitationSelection({super.key});
@override
ClassInvitationSelectionController createState() =>
ClassInvitationSelectionController();
}
class ClassInvitationSelectionController
extends State<ClassInvitationSelection> {
TextEditingController controller = TextEditingController();
late String currentSearchTerm;
bool loading = true;
List<User> allClassParticipants = [];
List<User> allChatParticipants = [];
List<User> classParticipantsFilteredByChat = [];
///Class participants filtered by chat participants and any search query
List<User> foundProfiles = [];
Timer? coolDown;
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
StreamSubscription<SyncUpdate>? _spaceSubscription;
void inviteAction(BuildContext context, String id) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!;
if (OkCancelResult.ok !=
await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).inviteContactToGroup(room.name),
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).cancel,
)) {
return;
}
final success = await showFutureLoadingDialog(
context: context,
future: () => room.invite(id),
);
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
// #Pangea
// content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup),
content: Text(L10n.of(context).contactHasBeenInvitedToTheChat),
// Pangea#
),
);
}
}
void setDisplayListWithCoolDown(String text) {
coolDown?.cancel();
coolDown = Timer(
const Duration(milliseconds: 0),
() => _setfoundProfiles(context, text),
);
}
void _setfoundProfiles(BuildContext context, String text) {
coolDown?.cancel();
// debugger(when: kDebugMode);
allClassParticipants = getClassParticipants(context);
allChatParticipants = getChatParticipants(context);
classParticipantsFilteredByChat = getClassParticipantsFilteredByChat();
currentSearchTerm = text;
foundProfiles = currentSearchTerm.isNotEmpty
? classParticipantsFilteredByChat
.where(
(user) =>
user.displayName?.contains(text) ??
false || user.id.contains(text),
)
.toList()
: classParticipantsFilteredByChat;
setState(() => loading = false);
}
Room? _getParentClass(BuildContext context) => Matrix.of(context)
.client
.rooms
.where(
(r) => r.isSpace,
)
.firstWhereOrNull(
(space) => space.spaceChildren.any(
(ithroom) => ithroom.roomId == roomId,
),
);
List<User> getClassParticipants(BuildContext context) {
final Room? parent = _getParentClass(context);
if (parent == null) return [];
final List<User> classParticipants =
parent.getParticipants([Membership.join]);
return classParticipants;
}
List<User> getChatParticipants(BuildContext context) => Matrix.of(context)
.client
.getRoomById(roomId!)!
.getParticipants([Membership.join, Membership.invite]).toList();
List<User> getClassParticipantsFilteredByChat() => allClassParticipants
.where(
(profile) =>
allChatParticipants.indexWhere((u) => u.id == profile.id) == -1,
)
.toList();
@override
void initState() {
Future.delayed(Duration.zero, () async {
final Room? classParent = _getParentClass(context);
await classParent
?.requestParticipants([Membership.join, Membership.invite]);
_setfoundProfiles(context, "");
_spaceSubscription = Matrix.of(context)
.client
.onSync
.stream
.where(
(event) =>
event.rooms?.join?.keys
.any((ithRoomId) => ithRoomId == classParent?.id) ??
false,
)
.listen(
(SyncUpdate syncUpdate) async {
debugPrint("updating lists");
await classParent
?.requestParticipants([Membership.join, Membership.invite]);
setState(() {});
},
);
});
super.initState();
}
@override
void dispose() {
_spaceSubscription?.cancel();
super.dispose();
}
@override
// Widget build(BuildContext context) => InvitationSelectionView(this);
Widget build(BuildContext context) => const SizedBox();
}

@ -70,9 +70,6 @@ void chatListHandleSpaceTap(
if (await space.leaveIfFull()) {
throw L10n.of(context).roomFull;
}
if (space.isSpace) {
space.joinAnalyticsRoomsInSpace();
}
setActiveSpaceAndCloseChat();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(

@ -103,8 +103,8 @@ Future<List<SpaceTeacher>> getReportTeachers(
final List<Room> otherSpaces = Matrix.of(context)
.client
.spacesImIn
.where((space) => !reportRoomParentSpaces.contains(space))
.rooms
.where((room) => room.isSpace && !reportRoomParentSpaces.contains(room))
.toList();
for (final space in otherSpaces) {

@ -33,21 +33,30 @@ class DownloadAnalyticsDialog extends StatefulWidget {
class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
bool _initialized = false;
bool _loading = false;
bool _finishedDownload = false;
bool _downloaded = false;
bool _joiningRooms = false;
bool _downloading = false;
bool get _loading => _joiningRooms || _downloading || !_initialized;
String? _error;
Map<String, int> _downloadStatues = {};
Map<String, int> _downloadStatuses = {};
@override
void initState() {
super.initState();
widget.space
.requestParticipants([Membership.join], false, true).whenComplete(() {
_resetDownloadStatuses();
_initialized = true;
if (mounted) setState(() {});
}).catchError((e, s) {
_initialize();
}
Future<void> _initialize() async {
try {
await widget.space.requestParticipants(
[Membership.join],
false,
true,
);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
@ -55,9 +64,26 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
"spaceID": widget.space.id,
},
);
if (mounted) setState(() => _error = e.toString());
return <User>[];
});
} finally {
if (mounted) setState(() => _initialized = true);
}
}
DownloadType _downloadType = DownloadType.csv;
void _setDownloadType(DownloadType type) {
_clean();
if (mounted) setState(() => _downloadType = type);
}
void _clean() {
_error = null;
_joiningRooms = false;
_downloading = false;
_downloaded = false;
_downloadStatuses = Map.fromEntries(
_usersToDownload.map((user) => MapEntry(user.id, 0)),
);
}
List<User> get _usersToDownload => widget.space
@ -66,13 +92,20 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
.toList();
Color _downloadStatusColor(String userID) {
final status = _downloadStatues[userID];
final status = _downloadStatuses[userID];
if (status == 1) return Colors.yellow;
if (status == 2) return Colors.green;
if (status == -1) return Colors.red;
if ((status ?? 0) < 0) return Colors.red;
return Colors.grey;
}
String? get _statusText {
if (_joiningRooms) return L10n.of(context).accessingMemberAnalytics;
if (_downloading) return L10n.of(context).downloading;
if (_downloaded) return L10n.of(context).downloadComplete;
return null;
}
Room? _userAnalyticsRoom(String userID) {
final rooms = widget.space.client.rooms;
final l2 = MatrixState.pangeaController.languageController.userL2?.langCode;
@ -84,12 +117,27 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
Future<void> _runDownload() async {
try {
_loading = true;
_error = null;
_resetDownloadStatuses();
if (mounted) setState(() {});
if (!mounted) return;
setState(() {
_error = null;
_joiningRooms = true;
});
await widget.space.joinAnalyticsRooms();
if (mounted) {
setState(() {
_joiningRooms = false;
_downloading = true;
});
}
await _downloadSpaceAnalytics();
if (mounted) setState(() => _finishedDownload = true);
if (mounted) {
setState(() {
_downloading = false;
_downloaded = true;
});
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
@ -98,11 +146,10 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
"spaceID": widget.space.id,
},
);
_resetDownloadStatuses();
_clean();
_error = e.toString();
if (mounted) setState(() {});
} finally {
if (mounted) setState(() => _loading = false);
}
}
@ -131,14 +178,17 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
Future<AnalyticsSummaryModel?> _getUserAnalyticsModel(String userID) async {
try {
setState(() => _downloadStatues[userID] = 1);
final userAnalyticsRoom = _userAnalyticsRoom(userID);
_downloadStatuses[userID] = userAnalyticsRoom != null ? 1 : -1;
if (mounted) setState(() {});
final constructEvents = await userAnalyticsRoom?.getAnalyticsEvents(
userId: userID,
);
if (constructEvents == null) {
setState(() => _downloadStatues[userID] = 0);
return null;
if (mounted) setState(() => _downloadStatuses[userID] = -1);
return AnalyticsSummaryModel.emptyModel(userID);
}
final List<OneConstructUse> uses = [];
@ -148,12 +198,12 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
final constructs = ConstructListModel(uses: uses);
final summary = AnalyticsSummaryModel.fromConstructListModel(
constructs,
userID,
constructs,
getCopy,
context,
);
setState(() => _downloadStatues[userID] = 2);
if (mounted) setState(() => _downloadStatuses[userID] = 2);
return summary;
} catch (e, s) {
ErrorHandler.logError(
@ -164,7 +214,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
"userID": userID,
},
);
setState(() => _downloadStatues[userID] = -1);
if (mounted) setState(() => _downloadStatuses[userID] = -2);
}
return null;
}
@ -175,7 +225,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
final List<CellValue> row = [];
for (int i = 0; i < AnalyticsSummaryEnum.values.length; i++) {
final key = AnalyticsSummaryEnum.values[i];
final value = summary.getValue(key);
final value = summary.getValue(key, context);
if (value is int) {
row.add(IntCellValue(value));
} else if (value is String) {
@ -232,7 +282,8 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
final row = [];
for (int i = 0; i < AnalyticsSummaryEnum.values.length; i++) {
final key = AnalyticsSummaryEnum.values[i];
final value = summary.getValue(key);
final value = summary.getValue(key, context);
if (value == null) continue;
value is List<String> ? row.add(value.join(", ")) : row.add(value);
}
rows.add(row);
@ -251,21 +302,6 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
use.lemma;
}
DownloadType _downloadType = DownloadType.csv;
void _setDownloadType(DownloadType type) {
_resetDownloadStatuses();
setState(() => _downloadType = type);
}
void _resetDownloadStatuses() {
_error = null;
_finishedDownload = false;
_downloadStatues = Map.fromEntries(
_usersToDownload.map((user) => MapEntry(user.id, 0)),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
@ -312,28 +348,26 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
itemCount: _usersToDownload.length,
itemBuilder: (context, index) {
final user = _usersToDownload[index];
final analyticsAvailable =
_userAnalyticsRoom(user.id) != null;
String tooltip = "";
if (!analyticsAvailable) {
if (_downloadStatuses[user.id] == -1) {
tooltip = L10n.of(context).analyticsNotAvailable;
} else if (_downloadStatues[user.id] == -1) {
} else if (_downloadStatuses[user.id] == -2) {
tooltip = L10n.of(context).failedFetchUserAnalytics;
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Opacity(
opacity: analyticsAvailable &&
_downloadStatues[user.id] != -1
? 1
: 0.5,
child: AnimatedOpacity(
duration: FluffyThemes.animationDuration,
opacity:
(_downloadStatuses[user.id] ?? 0) > 0 ? 1 : 0.5,
child: Row(
children: [
SizedBox(
width: 30,
child: !analyticsAvailable
width: 40,
height: 30,
child: (_downloadStatuses[user.id] ?? 0) < 0
? const Icon(
Icons.error_outline,
size: 16,
@ -377,7 +411,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
padding: const EdgeInsets.fromLTRB(8.0, 16.0, 8.0, 8.0),
child: OutlinedButton(
onPressed: _loading || !_initialized ? null : _runDownload,
child: _initialized
child: _initialized && !_loading
? Text(
_loading
? L10n.of(context).downloading
@ -385,17 +419,17 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
)
: const SizedBox(
height: 10,
width: 10,
child: CircularProgressIndicator.adaptive(),
width: 100,
child: LinearProgressIndicator(),
),
),
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _finishedDownload
child: _statusText != null
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text(L10n.of(context).downloadComplete),
child: Text(_statusText!),
)
: const SizedBox(),
),

@ -120,6 +120,7 @@ abstract class ClientManager {
PangeaEventTypes.capacity,
EventTypes.RoomPowerLevels,
PangeaEventTypes.userChosenEmoji,
EventTypes.RoomJoinRules,
// Pangea#
},
logLevel: kReleaseMode ? Level.warning : Level.verbose,

Loading…
Cancel
Save