ensure that users' analytics rooms are consistently made for users and that teachers are added to analytics rooms are soon as possible

pull/1183/head
ggurdin 2 years ago
parent f7fa048bde
commit 308bd9ee49

@ -3944,5 +3944,7 @@
"score": "Score", "score": "Score",
"accuracy": "Accuracy", "accuracy": "Accuracy",
"points": "Points", "points": "Points",
"noPaymentInfo": "No payment info necessary!" "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."
} }

@ -613,14 +613,14 @@ class ChatController extends State<ChatPageWithRoom>
useType: useType, useType: useType,
) )
.then( .then(
(String? msgEventId) { (String? msgEventId) async {
// #Pangea // #Pangea
setState(() { setState(() {
if (previousEdit != null) { if (previousEdit != null) {
edittingEvents.add(previousEdit.eventId); edittingEvents.add(previousEdit.eventId);
} }
}); });
// Pangea#
GoogleAnalytics.sendMessage( GoogleAnalytics.sendMessage(
room.id, room.id,
room.classCode, room.classCode,
@ -635,6 +635,8 @@ class ChatController extends State<ChatPageWithRoom>
return; return;
} }
// ensure that analytics room exists / is created for the active langCode
await room.ensureAnalyticsRoomExists();
pangeaController.myAnalytics.handleMessage( pangeaController.myAnalytics.handleMessage(
room, room,
RecentMessageRecord( RecentMessageRecord(

@ -2,6 +2,7 @@ import 'package:animations/animations.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
import 'package:fluffychat/pangea/choreographer/widgets/send_button.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/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
@ -330,7 +331,12 @@ class ChatInputRow extends StatelessWidget {
bottom: 6.0, bottom: 6.0,
top: 3.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( ? L10n.of(context)!.writeAMessageFlag(
activel1.languageEmoji ?? activel1.languageEmoji ??
activel1.getDisplayName(context) ?? activel1.getDisplayName(context) ??

@ -6,7 +6,9 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.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/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/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/add_to_space.dart'; import 'package:fluffychat/pangea/utils/add_to_space.dart';
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
@ -521,7 +523,7 @@ class ChatListController extends State<ChatList>
_invitedSpaceSubscription = pangeaController _invitedSpaceSubscription = pangeaController
.matrixState.client.onSync.stream .matrixState.client.onSync.stream
.where((event) => event.rooms?.invite != null) .where((event) => event.rooms?.invite != null)
.listen((event) { .listen((event) async {
for (final inviteEntry in event.rooms!.invite!.entries) { for (final inviteEntry in event.rooms!.invite!.entries) {
if (inviteEntry.value.inviteState == null) continue; if (inviteEntry.value.inviteState == null) continue;
final bool isSpace = inviteEntry.value.inviteState!.any( final bool isSpace = inviteEntry.value.inviteState!.any(
@ -529,7 +531,13 @@ class ChatListController extends State<ChatList>
event.type == EventTypes.RoomCreate && event.type == EventTypes.RoomCreate &&
event.content['type'] == 'm.space', event.content['type'] == 'm.space',
); );
if (!isSpace) continue; final bool isAnalytics = inviteEntry.value.inviteState!.any(
(event) =>
event.type == EventTypes.RoomCreate &&
event.content['type'] == PangeaRoomTypes.analytics,
);
if (isSpace) {
final String spaceId = inviteEntry.key; final String spaceId = inviteEntry.key;
final Room? space = pangeaController.matrixState.client.getRoomById( final Room? space = pangeaController.matrixState.client.getRoomById(
spaceId, spaceId,
@ -542,6 +550,22 @@ class ChatListController extends State<ChatList>
); );
} }
} }
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;
}
}
}); });
_subscriptionStatusStream ??= pangeaController _subscriptionStatusStream ??= pangeaController
@ -819,6 +843,7 @@ class ChatListController extends State<ChatList>
pangeaController.afterSyncAndFirstLoginInitialization(context); pangeaController.afterSyncAndFirstLoginInitialization(context);
await pangeaController.inviteBotToExistingSpaces(); await pangeaController.inviteBotToExistingSpaces();
await pangeaController.setPangeaPushRules(); await pangeaController.setPangeaPushRules();
await client.migrateAnalyticsRooms();
} else { } else {
ErrorHandler.logError( ErrorHandler.logError(
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",

@ -69,7 +69,7 @@ class ClientChooserButton extends StatelessWidget {
), ),
), ),
PopupMenuItem( PopupMenuItem(
enabled: matrix.client.classesAndExchangesImIn.isNotEmpty, enabled: matrix.client.allMyAnalyticsRooms.isNotEmpty,
value: SettingsAction.myAnalytics, value: SettingsAction.myAnalytics,
child: Row( child: Row(
children: [ children: [

@ -7,6 +7,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
import 'package:fluffychat/pangea/utils/archive_space.dart'; import 'package:fluffychat/pangea/utils/archive_space.dart';
@ -411,6 +412,18 @@ class _SpaceViewState extends State<SpaceView> {
} }
setState(() => refreshing = false); setState(() => refreshing = false);
} }
bool includeSpaceChild(sc, matchingSpaceChildren) {
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,
);
return !isAnalyticsRoom && (isMember || isSuggested);
}
// Pangea# // Pangea#
@override @override
@ -479,7 +492,7 @@ class _SpaceViewState extends State<SpaceView> {
) )
: L10n.of(context)!.youreInvited, : L10n.of(context)!.youreInvited,
), ),
if (rootSpace.locked ?? false) if (rootSpace.locked)
const Padding( const Padding(
padding: EdgeInsets.only(left: 4.0), padding: EdgeInsets.only(left: 4.0),
child: Icon( child: Icon(
@ -618,24 +631,17 @@ class _SpaceViewState extends State<SpaceView> {
.contains(spaceChild.roomId), .contains(spaceChild.roomId),
) )
.toList(); .toList();
spaceChildren = spaceChildren spaceChildren = spaceChildren
.where( .where(
(spaceChild) => (sc) => includeSpaceChild(
matchingSpaceChildren.any( sc,
(matchingSpaceChild) => matchingSpaceChildren,
matchingSpaceChild.roomId ==
spaceChild.roomId &&
matchingSpaceChild.suggested == true,
) ||
[Membership.join, Membership.invite].contains(
Matrix.of(context)
.client
.getRoomById(spaceChild.roomId)
?.membership,
), ),
) )
.toList(); .toList();
} }
spaceChildren.sort((a, b) { spaceChildren.sort((a, b) {
final bool aIsSpace = a.roomType == 'm.space'; final bool aIsSpace = a.roomType == 'm.space';
final bool bIsSpace = b.roomType == 'm.space'; final bool bIsSpace = b.roomType == 'm.space';

@ -157,7 +157,6 @@ class InvitationSelectionController extends State<InvitationSelection> {
//#Pangea //#Pangea
// future: () => room.invite(id), // future: () => room.invite(id),
future: () async { future: () async {
await room.invite(id);
if (mode == InvitationSelectionMode.admin) { if (mode == InvitationSelectionMode.admin) {
await inviteTeacherAction(room, id); await inviteTeacherAction(room, id);
} }
@ -175,7 +174,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
// #Pangea // #Pangea
Future<void> inviteTeacherAction(Room room, String id) async { Future<void> inviteTeacherAction(Room room, String id) async {
room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); await room.invite(id);
await room.setPower(id, ClassDefaultValues.powerLevelOfAdmin);
if (room.isSpace) { if (room.isSpace) {
for (final spaceChild in room.spaceChildren) { for (final spaceChild in room.spaceChildren) {
if (spaceChild.roomId == null) continue; if (spaceChild.roomId == null) continue;

@ -211,7 +211,8 @@ class Choreographer {
final CanSendStatus canSendStatus = final CanSendStatus canSendStatus =
pangeaController.subscriptionController.canSendStatus; pangeaController.subscriptionController.canSendStatus;
if (canSendStatus != CanSendStatus.subscribed) { if (canSendStatus != CanSendStatus.subscribed ||
(!igcEnabled && !itEnabled)) {
return; return;
} }

@ -120,21 +120,41 @@ class ClassController extends BaseController {
if (classChunk == null) { if (classChunk == null) {
ClassCodeUtil.messageSnack( ClassCodeUtil.messageSnack(
context, L10n.of(context)!.unableToFindClass); context,
L10n.of(context)!.unableToFindClass,
);
return; return;
} }
if (Matrix.of(context) if (_pangeaController.matrixState.client.rooms
.client
.rooms
.any((room) => room.id == classChunk.roomId)) { .any((room) => room.id == classChunk.roomId)) {
setActiveSpaceIdInChatListController(classChunk.roomId); setActiveSpaceIdInChatListController(classChunk.roomId);
ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
return; return;
} }
await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); await _pangeaController.matrixState.client.joinRoom(classChunk.roomId);
setActiveSpaceIdInChatListController(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); GoogleAnalytics.joinClass(classCode);
return; return;
} catch (err) { } catch (err) {

@ -90,7 +90,7 @@ class MyAnalyticsController {
} }
final Room analyticsRoom = await _pangeaController.matrixState.client final Room analyticsRoom = await _pangeaController.matrixState.client
.getMyAnalyticsRoom(langCode); .getMyAnalyticsRoom(langCode);
analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom();
final List<Future<void>> saveFutures = []; final List<Future<void>> saveFutures = [];
for (final uses in aggregatedVocabUse.entries) { for (final uses in aggregatedVocabUse.entries) {
debugPrint("saving of type ${uses.value.first.constructType}"); debugPrint("saving of type ${uses.value.first.constructType}");

@ -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/user_controller.dart';
import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension.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/guard/p_vguard.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
@ -272,6 +273,16 @@ class PangeaController {
} }
Future<void> setPangeaPushRules() async { Future<void> setPangeaPushRules() async {
final List<Room> 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( if (!(matrixState.client.globalPushRules?.override?.any(
(element) => element.ruleId == PangeaEventTypes.textToSpeechRule, (element) => element.ruleId == PangeaEventTypes.textToSpeechRule,
) ?? ) ??

@ -168,14 +168,20 @@ extension PangeaClient on Client {
// BotName.localBot, // BotName.localBot,
BotName.byEnvironment, BotName.byEnvironment,
], ],
visibility: Visibility.private,
roomAliasName: "${userID!.localpart}_${langCode}_analytics",
); );
if (getRoomById(roomID) == null) { if (getRoomById(roomID) == null) {
// Wait for room actually appears in sync // Wait for room actually appears in sync
await waitForRoomInSync(roomID, join: true); 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)!; return getRoomById(roomID)!;
} }
@ -245,4 +251,85 @@ extension PangeaClient on Client {
editEvents.add(originalEvent); editEvents.add(originalEvent);
return editEvents.slice(1).map((e) => e.eventId).toList(); return editEvents.slice(1).map((e) => e.eventId).toList();
} }
// Get all my analytics rooms
List<Room> 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<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);
}
// 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 = [];
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<void> inviteAllTeachersToAllAnalyticsRooms() async {
final List<Future> 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<void> joinAnalyticsRoomsInAllSpaces() async {
final List<Future> 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<void> 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<void> migrateAnalyticsRooms() async {
await updateAnalyticsRoomVisibility();
await addAnalyticsRoomsToAllSpaces();
await inviteAllTeachersToAllAnalyticsRooms();
await joinInvitedAnalyticsRooms();
await joinAnalyticsRoomsInAllSpaces();
}
} }

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.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 /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event
Future<Event?> _createStudentAnalyticsEvent() async { Future<Event?> _createStudentAnalyticsEvent() async {
try { try {
await postLoad();
if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) {
ErrorHandler.logError( ErrorHandler.logError(
m: "null powerLevels in createStudentAnalytics", m: "null powerLevels in createStudentAnalytics",
@ -453,7 +455,7 @@ extension PangeaRoom on Room {
debugger(when: kDebugMode); debugger(when: kDebugMode);
throw Exception("null userId in createStudentAnalytics"); throw Exception("null userId in createStudentAnalytics");
} }
await postLoad();
final String eventId = await client.setRoomStateWithKey( final String eventId = await client.setRoomStateWithKey(
id, id,
PangeaEventTypes.studentAnalyticsSummary, PangeaEventTypes.studentAnalyticsSummary,
@ -791,31 +793,6 @@ extension PangeaRoom on Room {
} }
} }
Future<void> 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 /// update state event and return eventId
Future<String> updateStateEvent(Event stateEvent) { Future<String> updateStateEvent(Event stateEvent) {
if (stateEvent.stateKey == null) { if (stateEvent.stateKey == null) {
@ -1059,4 +1036,299 @@ extension PangeaRoom on Room {
getState(PangeaEventTypes.botOptions)?.content ?? {}, getState(PangeaEventTypes.botOptions)?.content ?? {},
); );
} }
// 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.classesAndExchangesImStudyingIn)) {
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
if (space.canIAddSpaceChild(null)) {
try {
await space.setSpaceChild(id);
} catch (err) {
debugPrint(
"Failed to add analytics room for student ${client.userID} to space ${space.id}",
);
Sentry.addBreadcrumb(
Breadcrumb(
message: "Failed to add analytics room to space ${space.id}",
),
);
}
}
}
}
// Add all analytics rooms to space
// Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space
Future<void> addAnalyticsRoomsToSpace() async {
if (!isSpace) {
debugPrint("addAnalyticsRoomsToSpace called on non-space room");
Sentry.addBreadcrumb(
Breadcrumb(
message: "addAnalyticsRoomsToSpace called on non-space room",
),
);
return;
}
await postLoad();
if (!canIAddSpaceChild(null)) {
debugPrint(
"addAnalyticsRoomsToSpace called on space without add permission",
);
Sentry.addBreadcrumb(
Breadcrumb(
message:
"addAnalyticsRoomsToSpace called on space without add permission",
),
);
return;
}
final List<Room> allMyAnalyticsRooms = client.allMyAnalyticsRooms;
for (final Room analyticsRoom in allMyAnalyticsRooms) {
// add analytics room to space if it hasn't already been added
if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) continue;
try {
await setSpaceChild(analyticsRoom.id);
} catch (err) {
debugPrint(
"Failed to add analytics room ${analyticsRoom.id} to space $id",
);
Sentry.addBreadcrumb(
Breadcrumb(
message: "Failed to add analytics room to space $id",
),
);
}
}
}
// 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;
}
// load all participants of analytics room
if (!participantListComplete) {
await requestParticipants();
}
final List<User> participants = getParticipants();
// invite any teachers who are not already in the room
for (final teacher in (await client.myTeachers)) {
if (!participants.any((p) => p.id == teacher.id)) {
try {
await invite(teacher.id);
} catch (err, s) {
debugPrint(
"Failed to invite teacher ${teacher.id} to analytics room $id",
);
ErrorHandler.logError(
e: err,
m: "Failed to invite teacher ${teacher.id} to analytics room $id",
s: s,
);
}
}
}
}
// Invite teachers of 1 space to all users' analytics rooms
Future<void> inviteSpaceTeachersToAnalyticsRooms() async {
if (!isSpace) {
debugPrint(
"inviteSpaceTeachersToAllAnalyticsRoom called on non-space room",
);
Sentry.addBreadcrumb(
Breadcrumb(
message:
"inviteSpaceTeachersToAllAnalyticsRoom called on non-space room",
),
);
return;
}
for (final Room analyticsRoom in client.allMyAnalyticsRooms) {
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}",
);
ErrorHandler.logError(
e: err,
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
s: s,
);
}
}
}
}
}
// 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;
}
// 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<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,
);
}
}
}
Future<void> 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<void> ensureAnalyticsRoomExists() async {
await postLoad();
if (firstLanguageSettings?.targetLanguage == null) return;
await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage);
}
// Check if teacher is in students' analytics rooms
// To warn teachers if some data might be missing because they have
// not yet joined a students' analytics room
// Future<bool> areAllStudentAnalyticsAvailable() async {
// if (!isSpace) {
// debugPrint("areAllStudentAnalyticsAvailable called on non-space room");
// Sentry.addBreadcrumb(
// Breadcrumb(
// message: "areAllStudentAnalyticsAvailable called on non-space room",
// ),
// );
// return false;
// }
// final String? spaceLangCode = firstLanguageSettings?.targetLanguage;
// if (spaceLangCode == null) {
// debugPrint(
// "areAllStudentAnalyticsAvailable called on space without language settings",
// );
// Sentry.addBreadcrumb(
// Breadcrumb(
// message:
// "areAllStudentAnalyticsAvailable called on space without language settings",
// ),
// );
// return false;
// }
// for (final User student in students) {
// final Room? studentAnalyticsRoom = client.analyticsRoomLocal(
// spaceLangCode,
// student.id,
// );
// if (studentAnalyticsRoom == null) {
// return false;
// }
// }
// return true;
// }
} }

@ -52,7 +52,11 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
child: Opacity( child: Opacity(
opacity: widget.enabled ? 1 : 0.5, opacity: widget.enabled ? 1 : 0.5,
child: Tooltip( 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( child: ListTile(
leading: widget.type == AnalyticsEntryType.privateChats leading: widget.type == AnalyticsEntryType.privateChats
? CircleAvatar( ? CircleAvatar(
@ -101,7 +105,7 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
: null, : null,
selected: widget.selected, selected: widget.selected,
enabled: widget.enabled, enabled: widget.enabled,
onTap: () => onTap: () {
(room?.isSpace ?? false) && widget.allowNavigateOnSelect (room?.isSpace ?? false) && widget.allowNavigateOnSelect
? context.go( ? context.go(
'/rooms/analytics/${room!.id}', '/rooms/analytics/${room!.id}',
@ -112,7 +116,8 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
widget.type, widget.type,
widget.displayName, widget.displayName,
), ),
), );
},
trailing: (room?.isSpace ?? false) && trailing: (room?.isSpace ?? false) &&
widget.type != AnalyticsEntryType.privateChats && widget.type != AnalyticsEntryType.privateChats &&
widget.allowNavigateOnSelect widget.allowNavigateOnSelect

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; 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/base_analytics_view.dart';
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -142,14 +143,29 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
} }
bool enableSelection(AnalyticsSelected? selectedParam) { bool enableSelection(AnalyticsSelected? selectedParam) {
return selectedView == BarChartViewSelection.grammar && if (selectedView == BarChartViewSelection.grammar) {
selectedParam?.type == AnalyticsEntryType.room if (selectedParam?.type == AnalyticsEntryType.room) {
? Matrix.of(context) return Matrix.of(context)
.client .client
.getRoomById(selectedParam!.id) .getRoomById(selectedParam!.id)
?.membership == ?.membership ==
Membership.join Membership.join;
: true; }
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 @override

@ -246,6 +246,15 @@ class BaseAnalyticsView extends StatelessWidget {
.widget .widget
.tabs[1] .tabs[1]
.allowNavigateOnSelect, .allowNavigateOnSelect,
enabled:
controller.enableSelection(
AnalyticsSelected(
item.id,
controller
.widget.tabs[1].type,
"",
),
),
), ),
) )
.toList(), .toList(),

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; import 'package:fluffychat/pangea/models/chart_analytics_model.dart';
@ -103,7 +104,11 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
students = classRoom!.students; students = classRoom!.students;
chats = response.rooms chats = response.rooms
.where((room) => room.roomId != classRoom!.id) .where(
(room) =>
room.roomId != classRoom!.id &&
room.roomType != PangeaRoomTypes.analytics,
)
.toList(); .toList();
chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1); chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
} }

@ -33,7 +33,7 @@ class ClassAnalyticsView extends StatelessWidget {
.map( .map(
(s) => TabItem( (s) => TabItem(
avatar: s.avatarUrl, avatar: s.avatarUrl,
displayName: s.displayName ?? "unknown", displayName: s.calcDisplayname(),
id: s.id, id: s.id,
), ),
) )

@ -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/constructs_analytics_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.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/date_time_extension.dart';
import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
@ -54,7 +55,7 @@ class ConstructListState extends State<ConstructList> {
selected: widget.selected, selected: widget.selected,
forceUpdate: true, forceUpdate: true,
) )
.then((_) => setState(() => initialized = true)); .whenComplete(() => setState(() => initialized = true));
} }
@override @override
@ -160,11 +161,11 @@ class ConstructListViewState extends State<ConstructListView> {
stateSub?.cancel(); stateSub?.cancel();
} }
@override // @override
void didUpdateWidget(ConstructListView oldWidget) { // void didUpdateWidget(ConstructListView oldWidget) {
super.didUpdateWidget(oldWidget); // super.didUpdateWidget(oldWidget);
fetchUses(); // fetchUses();
} // }
int get lemmaIndex => int get lemmaIndex =>
constructs?.indexWhere( constructs?.indexWhere(
@ -215,6 +216,7 @@ class ConstructListViewState extends State<ConstructListView> {
} }
setState(() => fetchingUses = true); setState(() => fetchingUses = true);
try {
final List<OneConstructUse> uses = currentConstruct!.content.uses; final List<OneConstructUse> uses = currentConstruct!.content.uses;
_msgEvents.clear(); _msgEvents.clear();
@ -228,6 +230,15 @@ class ConstructListViewState extends State<ConstructListView> {
_msgEvents.add(msgEvent!); _msgEvents.add(msgEvent!);
} }
setState(() => fetchingUses = false); 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}",
);
}
} }
List<ConstructEvent>? get constructs => List<ConstructEvent>? get constructs =>
@ -278,12 +289,10 @@ class ConstructListViewState extends State<ConstructListView> {
children: [ children: [
if (constructs![lemmaIndex].content.uses.length > if (constructs![lemmaIndex].content.uses.length >
_msgEvents.length) _msgEvents.length)
const Center( Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(L10n.of(context)!.roomDataMissing),
"Some data may be missing from rooms in which you are not a member.",
),
), ),
), ),
Expanded( Expanded(

@ -65,6 +65,9 @@ void chatListHandleSpaceTap(
context: context, context: context,
future: () async { future: () async {
await space.join(); await space.join();
if (space.isSpace) {
await space.joinAnalyticsRoomsInSpace();
}
setActiveSpaceAndCloseChat(); setActiveSpaceAndCloseChat();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

@ -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/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message.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/igc/word_data_card.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -62,6 +63,10 @@ class ToolbarDisplayController {
if (controller.selectMode) { if (controller.selectMode) {
controller.clearSelectedEvents(); controller.clearSelectedEvents();
} }
if (!MatrixState.pangeaController.languageController.languagesSet) {
pLanguageDialog(context, () {});
return;
}
focusNode.requestFocus(); focusNode.requestFocus();
final LayerLinkAndKey layerLinkAndKey = final LayerLinkAndKey layerLinkAndKey =
@ -345,8 +350,11 @@ class MessageToolbarState extends State<MessageToolbar> {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) { children: MessageMode.values.map((mode) {
if ([MessageMode.definition, MessageMode.textToSpeech, MessageMode.translation] if ([
.contains(mode) && MessageMode.definition,
MessageMode.textToSpeech,
MessageMode.translation,
].contains(mode) &&
widget.pangeaMessageEvent.isAudioMessage) { widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }

Loading…
Cancel
Save