ensure that users' analytics rooms are consistently made for users and that teachers are added to analytics rooms are soon as possible
This commit is contained in:
parent
f7fa048bde
commit
308bd9ee49
21 changed files with 597 additions and 110 deletions
|
|
@ -3944,5 +3944,7 @@
|
|||
"score": "Score",
|
||||
"accuracy": "Accuracy",
|
||||
"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,
|
||||
)
|
||||
.then(
|
||||
(String? msgEventId) {
|
||||
(String? msgEventId) async {
|
||||
// #Pangea
|
||||
setState(() {
|
||||
if (previousEdit != null) {
|
||||
edittingEvents.add(previousEdit.eventId);
|
||||
}
|
||||
});
|
||||
// Pangea#
|
||||
|
||||
GoogleAnalytics.sendMessage(
|
||||
room.id,
|
||||
room.classCode,
|
||||
|
|
@ -635,6 +635,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
return;
|
||||
}
|
||||
|
||||
// ensure that analytics room exists / is created for the active langCode
|
||||
await room.ensureAnalyticsRoomExists();
|
||||
pangeaController.myAnalytics.handleMessage(
|
||||
room,
|
||||
RecentMessageRecord(
|
||||
|
|
|
|||
|
|
@ -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) ??
|
||||
|
|
|
|||
|
|
@ -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<ChatList>
|
|||
_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<ChatList>
|
|||
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<ChatList>
|
|||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
await client.migrateAnalyticsRooms();
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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/utils/on_chat_tap.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/sync_update_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/archive_space.dart';
|
||||
|
|
@ -411,6 +412,18 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
}
|
||||
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#
|
||||
|
||||
@override
|
||||
|
|
@ -479,7 +492,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
)
|
||||
: L10n.of(context)!.youreInvited,
|
||||
),
|
||||
if (rootSpace.locked ?? false)
|
||||
if (rootSpace.locked)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 4.0),
|
||||
child: Icon(
|
||||
|
|
@ -618,24 +631,17 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
.contains(spaceChild.roomId),
|
||||
)
|
||||
.toList();
|
||||
|
||||
spaceChildren = spaceChildren
|
||||
.where(
|
||||
(spaceChild) =>
|
||||
matchingSpaceChildren.any(
|
||||
(matchingSpaceChild) =>
|
||||
matchingSpaceChild.roomId ==
|
||||
spaceChild.roomId &&
|
||||
matchingSpaceChild.suggested == true,
|
||||
) ||
|
||||
[Membership.join, Membership.invite].contains(
|
||||
Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(spaceChild.roomId)
|
||||
?.membership,
|
||||
),
|
||||
(sc) => includeSpaceChild(
|
||||
sc,
|
||||
matchingSpaceChildren,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
spaceChildren.sort((a, b) {
|
||||
final bool aIsSpace = a.roomType == 'm.space';
|
||||
final bool bIsSpace = b.roomType == 'm.space';
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||
//#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<InvitationSelection> {
|
|||
|
||||
// #Pangea
|
||||
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) {
|
||||
for (final spaceChild in room.spaceChildren) {
|
||||
if (spaceChild.roomId == null) continue;
|
||||
|
|
|
|||
|
|
@ -211,7 +211,8 @@ class Choreographer {
|
|||
final CanSendStatus canSendStatus =
|
||||
pangeaController.subscriptionController.canSendStatus;
|
||||
|
||||
if (canSendStatus != CanSendStatus.subscribed) {
|
||||
if (canSendStatus != CanSendStatus.subscribed ||
|
||||
(!igcEnabled && !itEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class MyAnalyticsController {
|
|||
}
|
||||
final Room analyticsRoom = await _pangeaController.matrixState.client
|
||||
.getMyAnalyticsRoom(langCode);
|
||||
analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom();
|
||||
|
||||
final List<Future<void>> saveFutures = [];
|
||||
for (final uses in aggregatedVocabUse.entries) {
|
||||
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/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<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(
|
||||
(element) => element.ruleId == PangeaEventTypes.textToSpeechRule,
|
||||
) ??
|
||||
|
|
|
|||
|
|
@ -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<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: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<Event?> _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<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
|
||||
Future<String> updateStateEvent(Event stateEvent) {
|
||||
if (stateEvent.stateKey == null) {
|
||||
|
|
@ -1059,4 +1036,299 @@ extension PangeaRoom on Room {
|
|||
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(
|
||||
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<AnalyticsListTile> {
|
|||
: 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
|
||||
|
|
|
|||
|
|
@ -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<BaseAnalyticsPage> {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -246,6 +246,15 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
.widget
|
||||
.tabs[1]
|
||||
.allowNavigateOnSelect,
|
||||
enabled:
|
||||
controller.enableSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller
|
||||
.widget.tabs[1].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
|
|
@ -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<ClassAnalyticsPage> {
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ClassAnalyticsView extends StatelessWidget {
|
|||
.map(
|
||||
(s) => TabItem(
|
||||
avatar: s.avatarUrl,
|
||||
displayName: s.displayName ?? "unknown",
|
||||
displayName: s.calcDisplayname(),
|
||||
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/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<ConstructList> {
|
|||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.then((_) => setState(() => initialized = true));
|
||||
.whenComplete(() => setState(() => initialized = true));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -160,11 +161,11 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
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<ConstructListView> {
|
|||
}
|
||||
|
||||
setState(() => fetchingUses = true);
|
||||
final List<OneConstructUse> uses = currentConstruct!.content.uses;
|
||||
_msgEvents.clear();
|
||||
try {
|
||||
final List<OneConstructUse> 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;
|
||||
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<ConstructEvent>? get constructs =>
|
||||
|
|
@ -278,12 +289,10 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<MessageToolbar> {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue