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",
"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();
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<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;
}
_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…
Cancel
Save