diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 84eb41c3c1..6677bc3d36 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3945,6 +3945,8 @@ "accuracy": "Accuracy", "points": "Points", "noPaymentInfo": "No payment info necessary!", + "studentAnalyticsNotAvailable": "Student data not currently available", + "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", "wordsPerMinute": "Words per minute" -} +} \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 907f9c5bf5..0b9078ebbe 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -622,14 +622,14 @@ class ChatController extends State useType: useType, ) .then( - (String? msgEventId) { + (String? msgEventId) async { // #Pangea setState(() { if (previousEdit != null) { edittingEvents.add(previousEdit.eventId); } }); - // Pangea# + GoogleAnalytics.sendMessage( room.id, room.classCode, @@ -644,6 +644,8 @@ class ChatController extends State return; } + // ensure that analytics room exists / is created for the active langCode + await room.ensureAnalyticsRoomExists(); pangeaController.myAnalytics.handleMessage( room, RecentMessageRecord( diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0af014aef0..fd8d95b5b3 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -2,6 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -330,7 +331,12 @@ class ChatInputRow extends StatelessWidget { bottom: 6.0, top: 3.0, ), - hintText: activel1 != null && activel2 != null + hintText: activel1 != null && + activel2 != null && + activel1.langCode != + LanguageKeys.unknownLanguage && + activel2.langCode != + LanguageKeys.unknownLanguage ? L10n.of(context)!.writeAMessageFlag( activel1.languageEmoji ?? activel1.getDisplayName(context) ?? diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index c39298d6c8..f96baa040e 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -6,7 +6,9 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.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/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/client_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/add_to_space.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; @@ -521,7 +523,7 @@ class ChatListController extends State _invitedSpaceSubscription = pangeaController .matrixState.client.onSync.stream .where((event) => event.rooms?.invite != null) - .listen((event) { + .listen((event) async { for (final inviteEntry in event.rooms!.invite!.entries) { if (inviteEntry.value.inviteState == null) continue; final bool isSpace = inviteEntry.value.inviteState!.any( @@ -529,17 +531,39 @@ class ChatListController extends State event.type == EventTypes.RoomCreate && event.content['type'] == 'm.space', ); - if (!isSpace) continue; - final String spaceId = inviteEntry.key; - final Room? space = pangeaController.matrixState.client.getRoomById( - spaceId, + final bool isAnalytics = inviteEntry.value.inviteState!.any( + (event) => + event.type == EventTypes.RoomCreate && + event.content['type'] == PangeaRoomTypes.analytics, ); - if (space != null) { - chatListHandleSpaceTap( - context, - this, - space, + + if (isSpace) { + final String spaceId = inviteEntry.key; + final Room? space = pangeaController.matrixState.client.getRoomById( + spaceId, ); + if (space != null) { + chatListHandleSpaceTap( + context, + this, + space, + ); + } + } + + if (isAnalytics) { + final Room? analyticsRoom = + pangeaController.matrixState.client.getRoomById(inviteEntry.key); + try { + await analyticsRoom?.join(); + } catch (err, s) { + ErrorHandler.logError( + m: "Failed to join analytics room", + e: err, + s: s, + ); + } + return; } } }); @@ -819,6 +843,7 @@ class ChatListController extends State pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); + await client.migrateAnalyticsRooms(); } else { ErrorHandler.logError( m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index af21a177a3..73b7497f6b 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -69,7 +69,7 @@ class ClientChooserButton extends StatelessWidget { ), ), PopupMenuItem( - enabled: matrix.client.classesAndExchangesImIn.isNotEmpty, + enabled: matrix.client.allMyAnalyticsRooms.isNotEmpty, value: SettingsAction.myAnalytics, child: Row( children: [ diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 52e128ddcd..5f2bd5027e 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -157,7 +157,6 @@ class InvitationSelectionController extends State { //#Pangea // future: () => room.invite(id), future: () async { - await room.invite(id); if (mode == InvitationSelectionMode.admin) { await inviteTeacherAction(room, id); } @@ -175,7 +174,8 @@ class InvitationSelectionController extends State { // #Pangea Future inviteTeacherAction(Room room, String id) async { - room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); + await room.invite(id); + await room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); if (room.isSpace) { for (final spaceChild in room.spaceChildren) { if (spaceChild.roomId == null) continue; diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 156f36cc88..3a26676c69 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -211,7 +211,8 @@ class Choreographer { final CanSendStatus canSendStatus = pangeaController.subscriptionController.canSendStatus; - if (canSendStatus != CanSendStatus.subscribed) { + if (canSendStatus != CanSendStatus.subscribed || + (!igcEnabled && !itEnabled)) { return; } diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 7706d21947..c0a8e60931 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -120,21 +120,41 @@ class ClassController extends BaseController { if (classChunk == null) { ClassCodeUtil.messageSnack( - context, L10n.of(context)!.unableToFindClass); + context, + L10n.of(context)!.unableToFindClass, + ); return; } - if (Matrix.of(context) - .client - .rooms + if (_pangeaController.matrixState.client.rooms .any((room) => room.id == classChunk.roomId)) { setActiveSpaceIdInChatListController(classChunk.roomId); ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; } await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); - setActiveSpaceIdInChatListController(classChunk.roomId); + if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) == + null) { + await _pangeaController.matrixState.client.waitForRoomInSync( + classChunk.roomId, + join: true, + ); + } + + // add the user's analytics room to this joined space + // so their teachers can join them via the space hierarchy + final Room? joinedSpace = + _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + + // ensure that the user has an analytics room for this space's language + await joinedSpace?.ensureAnalyticsRoomExists(); + + // when possible, add user's analytics room the to space they joined + await joinedSpace?.addAnalyticsRoomsToSpace(); + + // and invite the space's teachers to the user's analytics rooms + await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); GoogleAnalytics.joinClass(classCode); return; } catch (err) { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 4b7c8cc967..808355b47f 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -90,7 +90,7 @@ class MyAnalyticsController { } final Room analyticsRoom = await _pangeaController.matrixState.client .getMyAnalyticsRoom(langCode); - analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom(); + final List> saveFutures = []; for (final uses in aggregatedVocabUse.entries) { debugPrint("saving of type ${uses.value.first.constructType}"); diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index e2ddc67149..204ef9307d 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -272,6 +273,16 @@ class PangeaController { } Future setPangeaPushRules() async { + final List analyticsRooms = + matrixState.client.rooms.where((room) => room.isAnalyticsRoom).toList(); + + for (final Room room in analyticsRooms) { + final pushRule = room.pushRuleState; + if (pushRule != PushRuleState.dontNotify) { + await room.setPushRuleState(PushRuleState.dontNotify); + } + } + if (!(matrixState.client.globalPushRules?.override?.any( (element) => element.ruleId == PangeaEventTypes.textToSpeechRule, ) ?? diff --git a/lib/pangea/extensions/client_extension.dart b/lib/pangea/extensions/client_extension.dart index 6a6c3359d4..b259d0c9e6 100644 --- a/lib/pangea/extensions/client_extension.dart +++ b/lib/pangea/extensions/client_extension.dart @@ -88,7 +88,7 @@ extension PangeaClient on Client { for (final classRoom in classesAndExchangesImIn) { 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) { + if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) { teachers.add(teacher); } } @@ -123,7 +123,7 @@ extension PangeaClient on Client { for (final room in rooms) { if (room.partial) await room.postLoad(); } - + final Room? analyticsRoom = analyticsRoomLocal(langCode); if (analyticsRoom != null) return analyticsRoom; @@ -168,14 +168,20 @@ extension PangeaClient on Client { // BotName.localBot, BotName.byEnvironment, ], - visibility: Visibility.private, - roomAliasName: "${userID!.localpart}_${langCode}_analytics", ); 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 + await analyticsRoom?.addAnalyticsRoomToSpaces(); + + // and invite all teachers to new analytics room + await analyticsRoom?.inviteTeachersToAnalyticsRoom(); return getRoomById(roomID)!; } @@ -245,4 +251,85 @@ extension PangeaClient on Client { editEvents.add(originalEvent); return editEvents.slice(1).map((e) => e.eventId).toList(); } + + // Get all my analytics rooms + List get allMyAnalyticsRooms => rooms + .where( + (e) => e.isAnalyticsRoomOfUser(userID!), + ) + .toList(); + + // migration function to change analytics rooms' vsibility to public + // so they will appear in the space hierarchy + Future updateAnalyticsRoomVisibility() async { + final List makePublicFutures = []; + for (final Room room in allMyAnalyticsRooms) { + final visability = await getRoomVisibilityOnDirectory(room.id); + if (visability != Visibility.public) { + await setRoomVisibilityOnDirectory( + room.id, + visibility: Visibility.public, + ); + } + } + await Future.wait(makePublicFutures); + } + + // Add all the users' analytics room to all the spaces the student studies in + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future addAnalyticsRoomsToAllSpaces() async { + final List addFutures = []; + for (final Room room in allMyAnalyticsRooms) { + addFutures.add(room.addAnalyticsRoomToSpaces()); + } + await Future.wait(addFutures); + } + + // Invite teachers to all my analytics room + // Handles case when students cannot add analytics room to space(s) + // So teacher is still able to get analytics data for this student + Future inviteAllTeachersToAllAnalyticsRooms() async { + final List inviteFutures = []; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom()); + } + await Future.wait(inviteFutures); + } + + // Join all analytics rooms in all spaces + // Allows teachers to join analytics rooms without being invited + Future joinAnalyticsRoomsInAllSpaces() async { + final List joinFutures = []; + for (final Room space in (await classesAndExchangesImTeaching)) { + joinFutures.add(space.joinAnalyticsRoomsInSpace()); + } + await Future.wait(joinFutures); + } + + // Join invited analytics rooms + // Checks for invites to any student analytics rooms + // Handles case of analytics rooms that can't be added to some space(s) + Future joinInvitedAnalyticsRooms() async { + for (final Room room in rooms) { + if (room.membership == Membership.invite && room.isAnalyticsRoom) { + try { + await room.join(); + } catch (err) { + debugPrint("Failed to join analytics room ${room.id}"); + } + } + } + } + + // helper function to join all relevant analytics rooms + // and set up those rooms to be joined by relevant teachers + Future migrateAnalyticsRooms() async { + await updateAnalyticsRoomVisibility(); + await addAnalyticsRoomsToAllSpaces(); + await inviteAllTeachersToAllAnalyticsRooms(); + await joinInvitedAnalyticsRooms(); + await joinAnalyticsRoomsInAllSpaces(); + } } diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 4e4f773dbd..bf152012a0 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -1,6 +1,7 @@ import 'dart:async'; 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_room_types.dart'; @@ -442,6 +443,7 @@ extension PangeaRoom on Room { /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event Future _createStudentAnalyticsEvent() async { try { + await postLoad(); if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { ErrorHandler.logError( m: "null powerLevels in createStudentAnalytics", @@ -453,7 +455,7 @@ extension PangeaRoom on Room { debugger(when: kDebugMode); throw Exception("null userId in createStudentAnalytics"); } - await postLoad(); + final String eventId = await client.setRoomStateWithKey( id, PangeaEventTypes.studentAnalyticsSummary, @@ -791,31 +793,6 @@ extension PangeaRoom on Room { } } - Future makeSureTeachersAreInvitedToAnalyticsRoom() async { - try { - if (!isAnalyticsRoom) { - throw Exception("not an analytics room"); - } - if (!participantListComplete) { - await requestParticipants(); - } - final toAdd = [ - ...getParticipants([Membership.invite, Membership.join]) - .map((e) => e.id), - BotName.byEnvironment, - ]; - for (final teacher in (await client.myTeachers)) { - if (!toAdd.contains(teacher.id)) { - debugPrint("inviting ${teacher.id} to analytics room"); - await invite(teacher.id); - } - } - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } - } - /// update state event and return eventId Future updateStateEvent(Event stateEvent) { if (stateEvent.stateKey == null) { @@ -1059,4 +1036,223 @@ extension PangeaRoom on Room { getState(PangeaEventTypes.botOptions)?.content ?? {}, ); } + + // add 1 analytics room to 1 space + Future 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(); + } + + if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; + if (canIAddSpaceChild(null)) { + 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", + ), + ); + } + } + } + + // Add analytics room to all spaces the user is a student in (1 analytics room to all spaces) + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future addAnalyticsRoomToSpaces() async { + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "addAnalyticsRoomToSpaces called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; + await space.addAnalyticsRoomToSpace(this); + } + } + + // Add all analytics rooms to space + // Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space + Future addAnalyticsRoomsToSpace() async { + await postLoad(); + final List allMyAnalyticsRooms = client.allMyAnalyticsRooms; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + await addAnalyticsRoomToSpace(analyticsRoom); + } + } + + // invite teachers of 1 space to 1 analytics room + Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { + if (!isSpace) { + debugPrint( + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ), + ); + return; + } + if (!analyticsRoom.participantListComplete) { + await analyticsRoom.requestParticipants(); + } + final List participants = analyticsRoom.getParticipants(); + for (final User teacher in (await teachers)) { + if (!participants.any((p) => p.id == teacher.id)) { + try { + await analyticsRoom.invite(teacher.id); + } catch (err, s) { + debugPrint( + "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + ); + ErrorHandler.logError( + e: err, + m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + s: s, + ); + } + } + } + } + + // Invite all teachers to 1 analytics room + // Handles case when students cannot add analytics room to space + // So teacher is still able to get analytics data for this student + Future inviteTeachersToAnalyticsRoom() async { + if (client.userID == null) { + debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called with null userId", + ), + ); + return; + } + + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + await space.inviteSpaceTeachersToAnalyticsRoom(this); + } + } + + // Invite teachers of 1 space to all users' analytics rooms + Future inviteSpaceTeachersToAnalyticsRooms() async { + for (final Room analyticsRoom in client.allMyAnalyticsRooms) { + await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); + } + } + + // Join analytics rooms in space + // Allows teachers to join analytics rooms without being invited + Future joinAnalyticsRoomsInSpace() async { + if (!isSpace) { + debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called on non-space room", + ), + ); + return; + } + + // 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) { + debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called by non-admin", + ), + ); + return; + } + + final spaceHierarchy = await client.getSpaceHierarchy( + id, + maxDepth: 1, + ); + + final List analyticsRoomIds = spaceHierarchy.rooms + .where( + (r) => r.roomType == PangeaRoomTypes.analytics, + ) + .map((r) => r.roomId) + .toList(); + + for (final String roomID in analyticsRoomIds) { + try { + await joinSpaceChild(roomID); + } catch (err, s) { + debugPrint("Failed to join analytics room $roomID in space $id"); + ErrorHandler.logError( + e: err, + m: "Failed to join analytics room $roomID in space $id", + s: s, + ); + } + } + } + + Future joinSpaceChild(String roomID) async { + final Room? child = client.getRoomById(roomID); + if (child == null) { + await client.joinRoom( + roomID, + serverName: spaceChildren + .firstWhereOrNull((child) => child.roomId == roomID) + ?.via, + ); + if (client.getRoomById(roomID) == null) { + await client.waitForRoomInSync(roomID, join: true); + } + return; + } + + if (![Membership.invite, Membership.join].contains(child.membership)) { + final waitForRoom = client.waitForRoomInSync( + roomID, + join: true, + ); + await child.join(); + await waitForRoom; + } + } + + // check if analytics room exists for a given language code + // and if not, create it + Future ensureAnalyticsRoomExists() async { + await postLoad(); + if (firstLanguageSettings?.targetLanguage == null) return; + await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage); + } } diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index ae81e957c0..ab35b26104 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -52,7 +52,11 @@ class AnalyticsListTileState extends State { child: Opacity( opacity: widget.enabled ? 1 : 0.5, child: Tooltip( - message: widget.enabled ? "" : L10n.of(context)!.joinToView, + message: widget.enabled + ? "" + : widget.type == AnalyticsEntryType.room + ? L10n.of(context)!.joinToView + : L10n.of(context)!.studentAnalyticsNotAvailable, child: ListTile( leading: widget.type == AnalyticsEntryType.privateChats ? CircleAvatar( @@ -101,18 +105,19 @@ class AnalyticsListTileState extends State { : null, selected: widget.selected, enabled: widget.enabled, - onTap: () => - (room?.isSpace ?? false) && widget.allowNavigateOnSelect - ? context.go( - '/rooms/analytics/${room!.id}', - ) - : widget.onTap( - AnalyticsSelected( - widget.id, - widget.type, - widget.displayName, - ), + onTap: () { + (room?.isSpace ?? false) && widget.allowNavigateOnSelect + ? context.go( + '/rooms/analytics/${room!.id}', + ) + : widget.onTap( + AnalyticsSelected( + widget.id, + widget.type, + widget.displayName, ), + ); + }, trailing: (room?.isSpace ?? false) && widget.type != AnalyticsEntryType.privateChats && widget.allowNavigateOnSelect diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 7182bc6aba..634a399805 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/extensions/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; @@ -142,14 +143,29 @@ class BaseAnalyticsController extends State { } bool enableSelection(AnalyticsSelected? selectedParam) { - return selectedView == BarChartViewSelection.grammar && - selectedParam?.type == AnalyticsEntryType.room - ? Matrix.of(context) + if (selectedView == BarChartViewSelection.grammar) { + if (selectedParam?.type == AnalyticsEntryType.room) { + return Matrix.of(context) .client .getRoomById(selectedParam!.id) ?.membership == - Membership.join - : true; + Membership.join; + } + + if (selectedParam?.type == AnalyticsEntryType.student) { + final String? langCode = + pangeaController.languageController.activeL2Code( + roomID: widget.defaultSelected.id, + ); + if (langCode == null) return false; + return Matrix.of(context).client.analyticsRoomLocal( + langCode, + selectedParam?.id, + ) != + null; + } + } + return true; } @override diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 1f331d2d51..86f179829b 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -246,6 +246,15 @@ class BaseAnalyticsView extends StatelessWidget { .widget .tabs[1] .allowNavigateOnSelect, + enabled: + controller.enableSelection( + AnalyticsSelected( + item.id, + controller + .widget.tabs[1].type, + "", + ), + ), ), ) .toList(), diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart index 0316d02cd5..877ae17884 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; @@ -103,7 +104,11 @@ class ClassAnalyticsV2Controller extends State { students = classRoom!.students; chats = response.rooms - .where((room) => room.roomId != classRoom!.id) + .where( + (room) => + room.roomId != classRoom!.id && + room.roomType != PangeaRoomTypes.analytics, + ) .toList(); chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1); } diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart index 0c05501721..dfb44e1063 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart @@ -33,7 +33,7 @@ class ClassAnalyticsView extends StatelessWidget { .map( (s) => TabItem( avatar: s.avatarUrl, - displayName: s.displayName ?? "unknown", + displayName: s.calcDisplayname(), id: s.id, ), ) diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index dd6c266184..838f766a3a 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_ev import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -54,7 +55,7 @@ class ConstructListState extends State { selected: widget.selected, forceUpdate: true, ) - .then((_) => setState(() => initialized = true)); + .whenComplete(() => setState(() => initialized = true)); } @override @@ -160,11 +161,11 @@ class ConstructListViewState extends State { stateSub?.cancel(); } - @override - void didUpdateWidget(ConstructListView oldWidget) { - super.didUpdateWidget(oldWidget); - fetchUses(); - } + // @override + // void didUpdateWidget(ConstructListView oldWidget) { + // super.didUpdateWidget(oldWidget); + // fetchUses(); + // } int get lemmaIndex => constructs?.indexWhere( @@ -215,19 +216,29 @@ class ConstructListViewState extends State { } setState(() => fetchingUses = true); - final List uses = currentConstruct!.content.uses; - _msgEvents.clear(); - - for (final OneConstructUse use in uses) { - final PangeaMessageEvent? msgEvent = await getMessageEvent(use); - final RepresentationEvent? repEvent = - msgEvent?.originalSent ?? msgEvent?.originalWritten; - if (repEvent?.choreo == null) { - continue; + try { + final List uses = currentConstruct!.content.uses; + _msgEvents.clear(); + + for (final OneConstructUse use in uses) { + final PangeaMessageEvent? msgEvent = await getMessageEvent(use); + final RepresentationEvent? repEvent = + msgEvent?.originalSent ?? msgEvent?.originalWritten; + if (repEvent?.choreo == null) { + continue; + } + _msgEvents.add(msgEvent!); } - _msgEvents.add(msgEvent!); + setState(() => fetchingUses = false); + } catch (err, s) { + setState(() => fetchingUses = false); + debugPrint("Error fetching uses: $err"); + ErrorHandler.logError( + e: err, + s: s, + m: "Failed to fetch uses for current construct ${currentConstruct?.content.lemma}", + ); } - setState(() => fetchingUses = false); } List? get constructs => @@ -278,12 +289,10 @@ class ConstructListViewState extends State { children: [ if (constructs![lemmaIndex].content.uses.length > _msgEvents.length) - const Center( + Center( child: Padding( - padding: EdgeInsets.all(8.0), - child: Text( - "Some data may be missing from rooms in which you are not a member.", - ), + padding: const EdgeInsets.all(8.0), + child: Text(L10n.of(context)!.roomDataMissing), ), ), Expanded( diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index 9828006615..01d99bda27 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -65,6 +65,9 @@ void chatListHandleSpaceTap( context: context, future: () async { await space.join(); + if (space.isSpace) { + await space.joinAnalyticsRoomsInSpace(); + } setActiveSpaceAndCloseChat(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 5c2f083d57..b2c61a3540 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -62,6 +63,10 @@ class ToolbarDisplayController { if (controller.selectMode) { controller.clearSelectedEvents(); } + if (!MatrixState.pangeaController.languageController.languagesSet) { + pLanguageDialog(context, () {}); + return; + } focusNode.requestFocus(); final LayerLinkAndKey layerLinkAndKey = @@ -345,8 +350,11 @@ class MessageToolbarState extends State { Row( mainAxisSize: MainAxisSize.min, children: MessageMode.values.map((mode) { - if ([MessageMode.definition, MessageMode.textToSpeech, MessageMode.translation] - .contains(mode) && + if ([ + MessageMode.definition, + MessageMode.textToSpeech, + MessageMode.translation, + ].contains(mode) && widget.pangeaMessageEvent.isAudioMessage) { return const SizedBox.shrink(); }