1389 space analytics download updates (#1395)
* feat: invite other admins to analytics rooms * fix: properly invite space admins to analytics rooms * feat: simplify process for getting space admins into analytics rooms * feat: add columns for over and under 80% correct use for original written and system generated uses
This commit is contained in:
parent
771bd4b6c3
commit
8cbe1ea1f7
19 changed files with 495 additions and 719 deletions
|
|
@ -4668,7 +4668,7 @@
|
|||
"numLemmasUsedCorrectly": "Number of lemmas used correctly at least once",
|
||||
"listLemmasUsedCorrectly": "List of lemmas used correctly at least once",
|
||||
"numLemmasUsedIncorrectly": "Number of lemmas used incorrectly at least once",
|
||||
"listLemmasUsedIncorrectly": "List of lemmas used incorrectly at least once",
|
||||
"listLemmasUsedIncorrectly": "Number of lemmas used correctly 0 times",
|
||||
"numLemmasSmallXP": "Number of lemmas with 0 - 30 XP",
|
||||
"numLemmasMediumXP": "Number of lemmas with 31 - 200 XP",
|
||||
"numLemmasLargeXP": "Number of lemmas with > 200 XP",
|
||||
|
|
@ -4677,8 +4677,10 @@
|
|||
"listLemmasLargeXP": "List of lemmas with > 200 XP",
|
||||
"numGrammarConcepts": "Number of grammar concepts",
|
||||
"listGrammarConcepts": "List of grammar concepts",
|
||||
"listGrammarConceptsUsedCorrectly": "List of grammar concepts used correctly at least 80% of the time",
|
||||
"listGrammarConceptsUsedIncorrectly": "List of grammar concepts used correctly less than 80% of the time",
|
||||
"listGrammarConceptsUsedCorrectly": "List of grammar concepts used correctly in original messages at least 80% of the time",
|
||||
"listGrammarConceptsUsedIncorrectly": "List of grammar concepts used correctly in original messages less than 80% of the time",
|
||||
"listGrammarConceptsUseCorrectlySystemGenerated": "List of grammar concepts chosen correctly from system-generated suggestions at least 80% of the time",
|
||||
"listGrammarConceptsUseIncorrectlySystemGenerated": "List of grammar concepts chosen correctly from system-generated suggestions less than 80% of the time",
|
||||
"incorrectGrammarConceptsUseCases": "Use cases of grammar concepts used incorrectly",
|
||||
"listGrammarConceptsSmallXP": "List of grammar concepts with 0 - 30 XP",
|
||||
"listGrammarConceptsMediumXP": "List of grammar concepts with 31 - 200 XP",
|
||||
|
|
@ -4696,5 +4698,10 @@
|
|||
"analyticsNotAvailable": "User analytics not available",
|
||||
"downloading": "Downloading...",
|
||||
"failedFetchUserAnalytics": "Failed to download user analytics",
|
||||
"downloadComplete": "Download complete!"
|
||||
}
|
||||
"downloadComplete": "Download complete!",
|
||||
"dataAvailable": "Data availability",
|
||||
"lemmasNeverUsedCorrectly": "Number of lemmas used correctly 0 times",
|
||||
"available": "Available",
|
||||
"unavailable": "Unavailable",
|
||||
"accessingMemberAnalytics": "Accessing member analytics..."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import 'package:fluffychat/pages/chat/send_file_dialog.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/app_version_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -1122,7 +1121,6 @@ class ChatListController extends State<ChatList>
|
|||
// #Pangea
|
||||
void _initPangeaControllers(Client client) {
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
client.migrateAnalyticsRooms();
|
||||
MatrixState.pangeaController.initControllers();
|
||||
if (mounted) {
|
||||
MatrixState.pangeaController.classController.joinCachedSpaceCode(context);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -36,17 +35,19 @@ class ClassController extends BaseController {
|
|||
/// to enable all other users to add child rooms to the space.
|
||||
void fixClassPowerLevels() {
|
||||
Future.wait(
|
||||
_pangeaController.matrixState.client.spacesImTeaching.map(
|
||||
(space) => space.setClassPowerLevels().catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"spaceID": space.id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
_pangeaController.matrixState.client.rooms
|
||||
.where((room) => room.isSpace && room.isRoomAdmin)
|
||||
.map(
|
||||
(space) => space.setClassPowerLevels().catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"spaceID": space.id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -148,11 +149,6 @@ class ClassController extends BaseController {
|
|||
return;
|
||||
}
|
||||
|
||||
// when possible, add user's analytics room the to space they joined
|
||||
room.addAnalyticsRoomsToSpace();
|
||||
|
||||
// and invite the space's teachers to the user's analytics rooms
|
||||
room.inviteSpaceTeachersToAnalyticsRooms();
|
||||
GoogleAnalytics.joinClass(classCode);
|
||||
|
||||
if (room.client.getRoomById(room.id)?.membership != Membership.join) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class GetAnalyticsController {
|
|||
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
|
||||
StreamController<AnalyticsStreamUpdate> analyticsStream =
|
||||
StreamController.broadcast();
|
||||
StreamSubscription? _joinSpaceSubscription;
|
||||
|
||||
ConstructListModel constructListModel = ConstructListModel(uses: []);
|
||||
Completer<void> initCompleter = Completer<void>();
|
||||
|
|
@ -72,10 +73,19 @@ class GetAnalyticsController {
|
|||
if (initCompleter.isCompleted) return;
|
||||
|
||||
try {
|
||||
_client.updateAnalyticsRoomVisibility();
|
||||
_client.addAnalyticsRoomsToSpaces();
|
||||
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.putAnalytics.analyticsUpdateStream.stream
|
||||
.listen(_onAnalyticsUpdate);
|
||||
|
||||
// When a newly-joined space comes through in a sync
|
||||
// update, add the analytics rooms to the space
|
||||
_joinSpaceSubscription ??= _client.onSync.stream
|
||||
.where(_client.isJoinSpaceSyncUpdate)
|
||||
.listen((_) => _client.addAnalyticsRoomsToSpaces());
|
||||
|
||||
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
|
||||
await _getConstructs();
|
||||
constructListModel.updateConstructs([
|
||||
|
|
@ -99,6 +109,8 @@ class GetAnalyticsController {
|
|||
constructListModel.dispose();
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
_analyticsUpdateSubscription = null;
|
||||
_joinSpaceSubscription?.cancel();
|
||||
_joinSpaceSubscription = null;
|
||||
initCompleter = Completer<void>();
|
||||
_cache.clear();
|
||||
// perMessage.dispose();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
enum AnalyticsSummaryEnum {
|
||||
username,
|
||||
dataAvailable,
|
||||
level,
|
||||
totalXP,
|
||||
numLemmas,
|
||||
|
|
@ -19,8 +20,10 @@ enum AnalyticsSummaryEnum {
|
|||
|
||||
numMorphConstructs,
|
||||
listMorphConstructs,
|
||||
listMorphConstructsUsedCorrectly,
|
||||
listMorphConstructsUsedIncorrectly,
|
||||
listMorphConstructsUsedCorrectlyOriginal,
|
||||
listMorphConstructsUsedIncorrectlyOriginal,
|
||||
listMorphConstructsUsedCorrectlySystem,
|
||||
listMorphConstructsUsedIncorrectlySystem,
|
||||
|
||||
// list morph 0 - 30 XP
|
||||
listMorphSmallXP,
|
||||
|
|
@ -45,6 +48,8 @@ extension AnalyticsSummaryEnumExtension on AnalyticsSummaryEnum {
|
|||
switch (this) {
|
||||
case AnalyticsSummaryEnum.username:
|
||||
return l10n.username;
|
||||
case AnalyticsSummaryEnum.dataAvailable:
|
||||
return l10n.dataAvailable;
|
||||
case AnalyticsSummaryEnum.level:
|
||||
return l10n.level;
|
||||
case AnalyticsSummaryEnum.totalXP:
|
||||
|
|
@ -65,10 +70,14 @@ extension AnalyticsSummaryEnumExtension on AnalyticsSummaryEnum {
|
|||
return l10n.numGrammarConcepts;
|
||||
case AnalyticsSummaryEnum.listMorphConstructs:
|
||||
return l10n.listGrammarConcepts;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectly:
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlyOriginal:
|
||||
return l10n.listGrammarConceptsUsedCorrectly;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectly:
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlyOriginal:
|
||||
return l10n.listGrammarConceptsUsedIncorrectly;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlySystem:
|
||||
return l10n.listGrammarConceptsUseCorrectlySystemGenerated;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlySystem:
|
||||
return l10n.listGrammarConceptsUseIncorrectlySystemGenerated;
|
||||
case AnalyticsSummaryEnum.listMorphSmallXP:
|
||||
return l10n.listGrammarConceptsSmallXP;
|
||||
case AnalyticsSummaryEnum.listMorphMediumXP:
|
||||
|
|
|
|||
|
|
@ -53,24 +53,15 @@ extension AnalyticsClientExtension on Client {
|
|||
},
|
||||
name: "$userID $langCode Analytics",
|
||||
topic: "This room stores learning analytics for $userID.",
|
||||
invite: [
|
||||
...(await myTeachers).map((e) => e.id),
|
||||
BotName.byEnvironment,
|
||||
],
|
||||
preset: CreateRoomPreset.publicChat,
|
||||
visibility: Visibility.private,
|
||||
);
|
||||
if (getRoomById(roomID) == null) {
|
||||
// Wait for room actually appears in sync
|
||||
await waitForRoomInSync(roomID, join: true);
|
||||
}
|
||||
|
||||
final Room? analyticsRoom = getRoomById(roomID);
|
||||
|
||||
// add this analytics room to all spaces so teachers can join them
|
||||
// via the space hierarchy
|
||||
analyticsRoom?.addAnalyticsRoomToSpaces();
|
||||
|
||||
// and invite all teachers to new analytics room
|
||||
analyticsRoom?.inviteTeachersToAnalyticsRoom();
|
||||
_addAnalyticsRoomsToSpaces();
|
||||
return getRoomById(roomID)!;
|
||||
}
|
||||
|
||||
|
|
@ -81,97 +72,96 @@ extension AnalyticsClientExtension on Client {
|
|||
)
|
||||
.toList();
|
||||
|
||||
// migration function to change analytics rooms' vsibility to public
|
||||
// so they will appear in the space hierarchy
|
||||
/// Update the visibility of all analytics rooms to private (do they don't show in search
|
||||
/// results) and set the join rules to public (so they come through in space hierarchy response)
|
||||
Future<void> _updateAnalyticsRoomVisibility() async {
|
||||
if (userID == null || userID == BotName.byEnvironment) return;
|
||||
final Random random = Random();
|
||||
|
||||
final visibilityFutures = allMyAnalyticsRooms.map((room) async {
|
||||
final visability = await getRoomVisibilityOnDirectory(room.id);
|
||||
if (visability != Visibility.private) {
|
||||
for (final analyticsRoom in allMyAnalyticsRooms) {
|
||||
if (userID == null) return;
|
||||
final visibility = await getRoomVisibilityOnDirectory(analyticsRoom.id);
|
||||
|
||||
// if making a call to the server (either to update visibility or join rules)
|
||||
// add a delay at the end of this interaction to prevent overloading the server
|
||||
int delay = 0;
|
||||
if (visibility != Visibility.private ||
|
||||
analyticsRoom.joinRules != JoinRules.public) {
|
||||
delay = random.nextInt(10);
|
||||
}
|
||||
|
||||
// don't show in search results
|
||||
if (visibility != Visibility.private) {
|
||||
await setRoomVisibilityOnDirectory(
|
||||
room.id,
|
||||
analyticsRoom.id,
|
||||
visibility: Visibility.private,
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
final joinRulesFutures = allMyAnalyticsRooms.map((room) async {
|
||||
if (room.joinRules != JoinRules.public) {
|
||||
await room.setJoinRules(JoinRules.public);
|
||||
// do show in space hierarchy
|
||||
if (analyticsRoom.joinRules != JoinRules.public) {
|
||||
await analyticsRoom.setJoinRules(JoinRules.public);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
await Future.wait(
|
||||
visibilityFutures + joinRulesFutures,
|
||||
);
|
||||
}
|
||||
|
||||
/// Add all the users' analytics room to all the spaces the user is studying in
|
||||
/// so teachers can join them via space hierarchy.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
void _addAnalyticsRoomsToAllSpaces() {
|
||||
if (userID == null || userID == BotName.byEnvironment) return;
|
||||
for (final Room room in allMyAnalyticsRooms) {
|
||||
room.addAnalyticsRoomToSpaces();
|
||||
await Future.delayed(Duration(seconds: delay));
|
||||
}
|
||||
}
|
||||
|
||||
/// Invite teachers to all my analytics room.
|
||||
/// Handles case when students cannot add analytics room to space(s)
|
||||
/// so teacher is still able to get analytics data for this student
|
||||
void _inviteAllTeachersToAllAnalyticsRooms() {
|
||||
/// Space admins join analytics rooms in spaces via the space hierarchy,
|
||||
/// so other members of the space need to add their analytics rooms to the space.
|
||||
Future<void> _addAnalyticsRoomsToSpaces() async {
|
||||
if (userID == null || userID == BotName.byEnvironment) return;
|
||||
for (final Room room in allMyAnalyticsRooms) {
|
||||
room.inviteTeachersToAnalyticsRoom();
|
||||
}
|
||||
}
|
||||
final spaces = rooms.where((room) => room.isSpace).toList();
|
||||
|
||||
// Join all analytics rooms in all spaces
|
||||
// Allows teachers to join analytics rooms without being invited
|
||||
Future<void> _joinAnalyticsRoomsInAllSpaces() async {
|
||||
for (final Room space in _spacesImTeaching) {
|
||||
// Each call to joinAnalyticsRoomsInSpace calls getSpaceHierarchy, which has a
|
||||
// strict rate limit. So we wait a second between each call to prevent a 429 error.
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() => space.joinAnalyticsRoomsInSpace(),
|
||||
final Random random = Random();
|
||||
for (final space in spaces) {
|
||||
if (userID == null) return;
|
||||
final List<Room> roomsNotAdded = allMyAnalyticsRooms.where((room) {
|
||||
return !space.spaceChildren.any((child) => child.roomId == room.id);
|
||||
}).toList();
|
||||
|
||||
if (roomsNotAdded.isEmpty) continue;
|
||||
|
||||
for (final analyticsRoom in roomsNotAdded) {
|
||||
if (userID == null) return;
|
||||
try {
|
||||
await space.setSpaceChild(analyticsRoom.id);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"spaceID": space.id,
|
||||
"analyticsRoomID": analyticsRoom.id,
|
||||
"userID": userID,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// add a delay before checking the next space to prevent overloading the server
|
||||
final delay = random.nextInt(10);
|
||||
debugPrint(
|
||||
"added ${roomsNotAdded.length} rooms to space ${space.id}, delay: $delay",
|
||||
);
|
||||
await Future.delayed(Duration(seconds: delay));
|
||||
}
|
||||
}
|
||||
|
||||
/// Join invited analytics rooms.
|
||||
/// Checks for invites to any student analytics rooms.
|
||||
/// Handles case of analytics rooms that can't be added to some space(s).
|
||||
void _joinInvitedAnalyticsRooms() {
|
||||
Future.wait(
|
||||
rooms
|
||||
.where(
|
||||
(room) =>
|
||||
room.membership == Membership.invite && room.isAnalyticsRoom,
|
||||
)
|
||||
.map(
|
||||
(room) => room.join().catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": room.id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to join all relevant analytics rooms
|
||||
/// and set up those rooms to be joined by other users.
|
||||
void _migrateAnalyticsRooms() {
|
||||
_updateAnalyticsRoomVisibility().then((_) {
|
||||
_addAnalyticsRoomsToAllSpaces();
|
||||
_inviteAllTeachersToAllAnalyticsRooms();
|
||||
_joinInvitedAnalyticsRooms();
|
||||
_joinAnalyticsRoomsInAllSpaces();
|
||||
});
|
||||
/// Check if sync update includes newly joined room. Used by the
|
||||
/// GetAnalyticsController to add analytics rooms to newly joined spaces.
|
||||
bool _isJoinSpaceSyncUpdate(SyncUpdate update) {
|
||||
if (update.rooms?.join == null) return false;
|
||||
return update.rooms!.join!.values
|
||||
.where(
|
||||
(e) =>
|
||||
e.state != null &&
|
||||
e.state!.any(
|
||||
(e) =>
|
||||
e.type == EventTypes.RoomCreate &&
|
||||
e.content['type'] == 'm.space',
|
||||
),
|
||||
)
|
||||
.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
|
@ -9,13 +10,11 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
||||
part "client_analytics_extension.dart";
|
||||
part "general_info_extension.dart";
|
||||
part "space_extension.dart";
|
||||
|
||||
extension PangeaClient on Client {
|
||||
// analytics
|
||||
|
|
@ -33,22 +32,18 @@ extension PangeaClient on Client {
|
|||
|
||||
List<Room> get allMyAnalyticsRooms => _allMyAnalyticsRooms;
|
||||
|
||||
/// Update the visibility of all analytics rooms to private (do they don't show in search
|
||||
/// results) and set the join rules to public (so they come through in space hierarchy response)
|
||||
Future<void> updateAnalyticsRoomVisibility() async =>
|
||||
await _updateAnalyticsRoomVisibility();
|
||||
_updateAnalyticsRoomVisibility();
|
||||
|
||||
/// Helper function to join all relevant analytics rooms
|
||||
/// and set up those rooms to be joined by other users.
|
||||
void migrateAnalyticsRooms() => _migrateAnalyticsRooms();
|
||||
/// Space admins join analytics rooms in spaces via the space hierarchy,
|
||||
/// so other members of the space need to add their analytics rooms to the space.
|
||||
Future<void> addAnalyticsRoomsToSpaces() async =>
|
||||
_addAnalyticsRoomsToSpaces();
|
||||
|
||||
// spaces
|
||||
|
||||
List<Room> get spacesImTeaching => _spacesImTeaching;
|
||||
|
||||
List<Room> get spacesImAStudentIn => _spacesImStudyingIn;
|
||||
|
||||
List<Room> get spacesImIn => _spacesImIn;
|
||||
|
||||
PangeaRoomRules? get lastUpdatedRoomRules => _lastUpdatedRoomRules;
|
||||
bool isJoinSpaceSyncUpdate(SyncUpdate update) =>
|
||||
_isJoinSpaceSyncUpdate(update);
|
||||
|
||||
// general_info
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ part of "client_extension.dart";
|
|||
extension GeneralInfoClientExtension on Client {
|
||||
Future<List<User>> get _myTeachers async {
|
||||
final List<User> teachers = [];
|
||||
for (final classRoom in spacesImIn) {
|
||||
final spaces = rooms.where((room) => room.isSpace);
|
||||
for (final classRoom in spaces) {
|
||||
for (final teacher in await classRoom.teachers) {
|
||||
// If person requesting list of teachers is a teacher in another classroom, don't add them to the list
|
||||
if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) {
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
part of "client_extension.dart";
|
||||
|
||||
extension SpaceClientExtension on Client {
|
||||
List<Room> get _spacesImTeaching =>
|
||||
rooms.where((e) => e.isSpace && e.isRoomAdmin).toList();
|
||||
|
||||
List<Room> get _spacesImStudyingIn =>
|
||||
rooms.where((e) => e.isSpace && !e.isRoomAdmin).toList();
|
||||
|
||||
List<Room> get _spacesImIn => rooms.where((e) => e.isSpace).toList();
|
||||
|
||||
PangeaRoomRules? get _lastUpdatedRoomRules => _spacesImTeaching
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
|
||||
// LanguageSettingsModel? get _lastUpdatedLanguageSettings => rooms
|
||||
// .where((room) => room.isSpace && room.languageSettingsUpdatedAt != null)
|
||||
// .sorted(
|
||||
// (a, b) => b.languageSettingsUpdatedAt!
|
||||
// .compareTo(a.languageSettingsUpdatedAt!),
|
||||
// )
|
||||
// .firstOrNull
|
||||
// ?.languageSettings;
|
||||
}
|
||||
|
|
@ -19,14 +19,11 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_code_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
|
|
@ -38,7 +35,6 @@ import '../../../config/app_config.dart';
|
|||
import '../../constants/pangea_event_types.dart';
|
||||
import '../../models/choreo_record.dart';
|
||||
import '../../models/representation_content_model.dart';
|
||||
import '../client_extension/client_extension.dart';
|
||||
|
||||
part "room_analytics_extension.dart";
|
||||
part "room_children_and_parents_extension.dart";
|
||||
|
|
@ -51,35 +47,7 @@ part "room_user_permissions_extension.dart";
|
|||
extension PangeaRoom on Room {
|
||||
// analytics
|
||||
|
||||
/// Join analytics rooms in space.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
Future<void> joinAnalyticsRoomsInSpace() async =>
|
||||
await _joinAnalyticsRoomsInSpace();
|
||||
|
||||
Future<void> addAnalyticsRoomToSpace(Room analyticsRoom) async =>
|
||||
await _addAnalyticsRoomToSpace(analyticsRoom);
|
||||
|
||||
/// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces).
|
||||
/// Enables teachers to join student analytics rooms via space hierarchy.
|
||||
/// Will not always work, as there may be spaces where students don't have permission to add chats,
|
||||
/// but allows teachers to join analytics rooms without being invited.
|
||||
void addAnalyticsRoomToSpaces() => _addAnalyticsRoomToSpaces();
|
||||
|
||||
/// Add all the user's analytics rooms to 1 space.
|
||||
void addAnalyticsRoomsToSpace() => _addAnalyticsRoomsToSpace();
|
||||
|
||||
/// Invite teachers of 1 space to 1 analytics room
|
||||
Future<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async =>
|
||||
await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
|
||||
|
||||
/// Invite all the user's teachers to 1 analytics room.
|
||||
/// Handles case when students cannot add analytics room to space
|
||||
/// so teacher is still able to get analytics data for this student.
|
||||
void inviteTeachersToAnalyticsRoom() => _inviteTeachersToAnalyticsRoom();
|
||||
|
||||
/// Invite teachers of 1 space to all users' analytics rooms
|
||||
void inviteSpaceTeachersToAnalyticsRooms() =>
|
||||
_inviteSpaceTeachersToAnalyticsRooms();
|
||||
Future<void> joinAnalyticsRooms() async => await _joinAnalyticsRooms();
|
||||
|
||||
Future<DateTime?> analyticsLastUpdated(String userId) async {
|
||||
return await _analyticsLastUpdated(userId);
|
||||
|
|
@ -165,9 +133,6 @@ extension PangeaRoom on Room {
|
|||
|
||||
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
|
||||
|
||||
Future<List<LanguageModel>> targetLanguages() async =>
|
||||
await _targetLanguages();
|
||||
|
||||
// events
|
||||
|
||||
Future<bool> leaveIfFull() async => await _leaveIfFull();
|
||||
|
|
|
|||
|
|
@ -1,156 +1,77 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension AnalyticsRoomExtension on Room {
|
||||
/// Join analytics rooms in space.
|
||||
/// Allows teachers to join analytics rooms without being invited.
|
||||
Future<void> _joinAnalyticsRoomsInSpace() async {
|
||||
try {
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
Future<List<SpaceRoomsChunk>> _getFullSpaceHierarchy() async {
|
||||
final resp = await client.getSpaceHierarchy(
|
||||
id,
|
||||
limit: 100,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
if (client.userID == null || !isRoomAdmin) return;
|
||||
final spaceHierarchy = await client.getSpaceHierarchy(
|
||||
final List<SpaceRoomsChunk> rooms = resp.rooms;
|
||||
String? nextBatch = resp.nextBatch;
|
||||
int tries = 0;
|
||||
|
||||
while (nextBatch != null && tries <= 5) {
|
||||
final nextResp = await client.getSpaceHierarchy(
|
||||
id,
|
||||
from: nextBatch,
|
||||
limit: 100,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
final List<String> analyticsRoomIds = spaceHierarchy.rooms
|
||||
.where((r) => r.roomType == PangeaRoomTypes.analytics)
|
||||
.map((r) => r.roomId)
|
||||
.toList();
|
||||
|
||||
await Future.wait(
|
||||
analyticsRoomIds.map(
|
||||
(roomID) => joinSpaceChild(roomID).catchError((err, s) {
|
||||
debugPrint("Failed to join analytics room $roomID in space $id");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to join analytics room $roomID in space $id",
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": roomID,
|
||||
"spaceID": id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"spaceID": id,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// add 1 analytics room to 1 space
|
||||
Future<void> _addAnalyticsRoomToSpace(Room analyticsRoom) async {
|
||||
if (!isSpace) {
|
||||
debugPrint("addAnalyticsRoomToSpace called on non-space room");
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "addAnalyticsRoomToSpace called on non-space room",
|
||||
),
|
||||
);
|
||||
return Future.value();
|
||||
rooms.addAll(nextResp.rooms);
|
||||
nextBatch = nextResp.nextBatch;
|
||||
tries++;
|
||||
}
|
||||
|
||||
// Checks that user has permission to add child to space
|
||||
if (!canSendEvent(EventTypes.SpaceChild)) return;
|
||||
if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return;
|
||||
return rooms;
|
||||
}
|
||||
|
||||
try {
|
||||
await setSpaceChild(analyticsRoom.id);
|
||||
} catch (err) {
|
||||
debugPrint(
|
||||
"Failed to add analytics room ${analyticsRoom.id} for student to space $id",
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "Failed to add analytics room to space $id",
|
||||
),
|
||||
);
|
||||
Future<void> _joinAnalyticsRooms() async {
|
||||
final List<SpaceRoomsChunk> rooms = await _getFullSpaceHierarchy();
|
||||
|
||||
final unjoinedAnalyticsRooms = rooms.where(
|
||||
(room) {
|
||||
if (room.roomType != PangeaRoomTypes.analytics) return false;
|
||||
final matchingRoom = client.rooms.firstWhereOrNull(
|
||||
(r) => r.id == room.roomId,
|
||||
);
|
||||
return matchingRoom == null ||
|
||||
matchingRoom.membership != Membership.join;
|
||||
},
|
||||
).toList();
|
||||
|
||||
const batchSize = 5;
|
||||
int batchNum = 0;
|
||||
while (batchSize * batchNum < unjoinedAnalyticsRooms.length) {
|
||||
final batch =
|
||||
unjoinedAnalyticsRooms.sublist(batchSize * batchNum).take(batchSize);
|
||||
|
||||
batchNum++;
|
||||
for (final analyticsRoom in batch) {
|
||||
try {
|
||||
final syncFuture =
|
||||
client.waitForRoomInSync(analyticsRoom.roomId, join: true);
|
||||
await client.joinRoom(analyticsRoom.roomId);
|
||||
await syncFuture;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"spaceID": id,
|
||||
"roomID": analyticsRoom.roomId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (batchSize * batchNum < unjoinedAnalyticsRooms.length) {
|
||||
await Future.delayed(const Duration(milliseconds: 7500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add analytics room to all spaces the user is a student in (1 analytics room to all spaces).
|
||||
/// Enables teachers to join student analytics rooms via space hierarchy.
|
||||
/// Will not always work, as there may be spaces where students don't have permission to add chats,
|
||||
/// but allows teachers to join analytics rooms without being invited.
|
||||
void _addAnalyticsRoomToSpaces() {
|
||||
if (!isAnalyticsRoomOfUser(client.userID!)) return;
|
||||
Future.wait(
|
||||
client.spacesImAStudentIn
|
||||
.where((space) => !space.spaceChildren.any((sc) => sc.roomId == id))
|
||||
.map((space) => space.addAnalyticsRoomToSpace(this)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Add all the user's analytics rooms to 1 space.
|
||||
void _addAnalyticsRoomsToSpace() {
|
||||
Future.wait(
|
||||
client.allMyAnalyticsRooms.map((room) => addAnalyticsRoomToSpace(room)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Invite teachers of 1 space to 1 analytics room
|
||||
Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async {
|
||||
if (!isSpace) return;
|
||||
if (!analyticsRoom.participantListComplete) {
|
||||
await analyticsRoom.requestParticipants();
|
||||
}
|
||||
|
||||
final List<User> participants = analyticsRoom.getParticipants();
|
||||
final List<User> uninvitedTeachers = (await teachers)
|
||||
.where((teacher) => !participants.contains(teacher))
|
||||
.toList();
|
||||
|
||||
if (analyticsRoom.canSendEvent(EventTypes.RoomMember)) {
|
||||
Future.wait(
|
||||
uninvitedTeachers.map(
|
||||
(teacher) => analyticsRoom.invite(teacher.id).catchError((err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}",
|
||||
s: s,
|
||||
data: {
|
||||
"teacherID": teacher.id,
|
||||
"analyticsRoomID": analyticsRoom.id,
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invite all the user's teachers to 1 analytics room.
|
||||
/// Handles case when students cannot add analytics room to space
|
||||
/// so teacher is still able to get analytics data for this student.
|
||||
void _inviteTeachersToAnalyticsRoom() {
|
||||
if (client.userID == null || !isAnalyticsRoomOfUser(client.userID!)) return;
|
||||
Future.wait(
|
||||
client.spacesImAStudentIn.map(
|
||||
(space) => inviteSpaceTeachersToAnalyticsRoom(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Invite teachers of 1 space to all users' analytics rooms
|
||||
void _inviteSpaceTeachersToAnalyticsRooms() {
|
||||
Future.wait(
|
||||
client.allMyAnalyticsRooms.map(
|
||||
(room) => inviteSpaceTeachersToAnalyticsRoom(room),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<DateTime?> _analyticsLastUpdated(String userId) async {
|
||||
final List<Event> events =
|
||||
await getRoomAnalyticsEvents(count: 1, userID: userId);
|
||||
|
|
|
|||
|
|
@ -110,35 +110,6 @@ extension SpaceRoomExtension on Room {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<List<LanguageModel>> _targetLanguages() async {
|
||||
await requestParticipants();
|
||||
final students = _students;
|
||||
|
||||
final Map<LanguageModel, int> langCounts = {};
|
||||
final List<Room> allRooms = client.rooms;
|
||||
for (final User student in students) {
|
||||
for (final Room room in allRooms) {
|
||||
if (!room.isAnalyticsRoomOfUser(student.id)) continue;
|
||||
final String? langCode = room.madeForLang;
|
||||
if (langCode == null ||
|
||||
langCode.isEmpty ||
|
||||
langCode == LanguageKeys.unknownLanguage) {
|
||||
continue;
|
||||
}
|
||||
final LanguageModel? lang = PangeaLanguage.byLangCode(langCode);
|
||||
if (lang == null) continue;
|
||||
langCounts[lang] ??= 0;
|
||||
langCounts[lang] = langCounts[lang]! + 1;
|
||||
}
|
||||
}
|
||||
// get a list of language models, sorted
|
||||
// by the number of students who are learning that language
|
||||
return langCounts.entries.map((entry) => entry.key).toList()
|
||||
..sort(
|
||||
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
);
|
||||
}
|
||||
|
||||
// DateTime? get _languageSettingsUpdatedAt {
|
||||
// if (!isSpace) return null;
|
||||
// return languageSettingsStateEvent?.originServerTs ?? creationTime;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/analytics/analytics_summary_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
|
|
@ -8,104 +10,150 @@ import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
|
|||
|
||||
class AnalyticsSummaryModel {
|
||||
String username;
|
||||
int level;
|
||||
int totalXP;
|
||||
bool dataAvailable;
|
||||
int? level;
|
||||
int? totalXP;
|
||||
|
||||
int numLemmas;
|
||||
int numLemmasUsedCorrectly;
|
||||
int numLemmasUsedIncorrectly;
|
||||
int? numLemmas;
|
||||
int? numLemmasUsedCorrectly;
|
||||
int? numLemmasUsedIncorrectly;
|
||||
|
||||
/// 0 - 30 XP
|
||||
int numLemmasSmallXP;
|
||||
int? numLemmasSmallXP;
|
||||
|
||||
/// 31 - 200 XP
|
||||
int numLemmasMediumXP;
|
||||
int? numLemmasMediumXP;
|
||||
|
||||
/// > 200 XP
|
||||
int numLemmasLargeXP;
|
||||
int? numLemmasLargeXP;
|
||||
|
||||
int numMorphConstructs;
|
||||
List<String> listMorphConstructs;
|
||||
List<String> listMorphConstructsUsedCorrectly;
|
||||
List<String> listMorphConstructsUsedIncorrectly;
|
||||
int? numMorphConstructs;
|
||||
List<String>? listMorphConstructs;
|
||||
List<String>? listMorphConstructsUsedCorrectlyOriginal;
|
||||
List<String>? listMorphConstructsUsedIncorrectlyOriginal;
|
||||
List<String>? listMorphConstructsUsedCorrectlySystem;
|
||||
List<String>? listMorphConstructsUsedIncorrectlySystem;
|
||||
|
||||
// list morph 0 - 30 XP
|
||||
List<String> listMorphSmallXP;
|
||||
List<String>? listMorphSmallXP;
|
||||
|
||||
// list morph 31 - 200 XP
|
||||
List<String> listMorphMediumXP;
|
||||
List<String>? listMorphMediumXP;
|
||||
|
||||
// list morph 200 - 500 XP
|
||||
List<String> listMorphLargeXP;
|
||||
List<String>? listMorphLargeXP;
|
||||
|
||||
// list morph > 500 XP
|
||||
List<String> listMorphHugeXP;
|
||||
List<String>? listMorphHugeXP;
|
||||
|
||||
int numMessagesSent;
|
||||
int numWordsTyped;
|
||||
int numChoicesCorrect;
|
||||
int numChoicesIncorrect;
|
||||
int? numMessagesSent;
|
||||
int? numWordsTyped;
|
||||
int? numChoicesCorrect;
|
||||
int? numChoicesIncorrect;
|
||||
|
||||
AnalyticsSummaryModel({
|
||||
required this.username,
|
||||
required this.level,
|
||||
required this.totalXP,
|
||||
required this.numLemmas,
|
||||
required this.numLemmasUsedCorrectly,
|
||||
required this.numLemmasUsedIncorrectly,
|
||||
required this.numLemmasSmallXP,
|
||||
required this.numLemmasMediumXP,
|
||||
required this.numLemmasLargeXP,
|
||||
required this.numMorphConstructs,
|
||||
required this.listMorphConstructs,
|
||||
required this.listMorphConstructsUsedCorrectly,
|
||||
required this.listMorphConstructsUsedIncorrectly,
|
||||
required this.listMorphSmallXP,
|
||||
required this.listMorphMediumXP,
|
||||
required this.listMorphLargeXP,
|
||||
required this.listMorphHugeXP,
|
||||
required this.numMessagesSent,
|
||||
required this.numWordsTyped,
|
||||
required this.numChoicesCorrect,
|
||||
required this.numChoicesIncorrect,
|
||||
required this.dataAvailable,
|
||||
this.level,
|
||||
this.totalXP,
|
||||
this.numLemmas,
|
||||
this.numLemmasUsedCorrectly,
|
||||
this.numLemmasUsedIncorrectly,
|
||||
this.numLemmasSmallXP,
|
||||
this.numLemmasMediumXP,
|
||||
this.numLemmasLargeXP,
|
||||
this.numMorphConstructs,
|
||||
this.listMorphConstructs,
|
||||
this.listMorphConstructsUsedCorrectlyOriginal,
|
||||
this.listMorphConstructsUsedIncorrectlyOriginal,
|
||||
this.listMorphConstructsUsedCorrectlySystem,
|
||||
this.listMorphConstructsUsedIncorrectlySystem,
|
||||
this.listMorphSmallXP,
|
||||
this.listMorphMediumXP,
|
||||
this.listMorphLargeXP,
|
||||
this.listMorphHugeXP,
|
||||
this.numMessagesSent,
|
||||
this.numWordsTyped,
|
||||
this.numChoicesCorrect,
|
||||
this.numChoicesIncorrect,
|
||||
});
|
||||
|
||||
static AnalyticsSummaryModel emptyModel(String userID) {
|
||||
return AnalyticsSummaryModel(
|
||||
username: userID,
|
||||
dataAvailable: false,
|
||||
);
|
||||
}
|
||||
|
||||
static AnalyticsSummaryModel fromConstructListModel(
|
||||
ConstructListModel model,
|
||||
String userID,
|
||||
ConstructListModel? model,
|
||||
String Function(ConstructUses) getCopy,
|
||||
BuildContext context,
|
||||
) {
|
||||
final vocabLemmas = LemmasToUsesWrapper(
|
||||
model.lemmasToUses(type: ConstructTypeEnum.vocab),
|
||||
);
|
||||
final morphLemmas = LemmasToUsesWrapper(
|
||||
model.lemmasToUses(type: ConstructTypeEnum.morph),
|
||||
);
|
||||
final vocabLemmas = model != null
|
||||
? LemmasToUsesWrapper(
|
||||
model.lemmasToUses(type: ConstructTypeEnum.vocab),
|
||||
)
|
||||
: null;
|
||||
final morphLemmas = model != null
|
||||
? LemmasToUsesWrapper(
|
||||
model.lemmasToUses(type: ConstructTypeEnum.morph),
|
||||
)
|
||||
: null;
|
||||
|
||||
final morphLemmasPercentCorrect = morphLemmas.lemmasByPercent(
|
||||
percent: 0.8,
|
||||
getCopy: getCopy,
|
||||
);
|
||||
final List<String> correctOriginalUseLemmas = [];
|
||||
final List<String> correctSystemUseLemmas = [];
|
||||
final List<String> incorrectOriginalUseLemmas = [];
|
||||
final List<String> incorrectSystemUseLemmas = [];
|
||||
|
||||
final vocabLemmasCorrect = vocabLemmas.lemmasByCorrectUse(getCopy: getCopy);
|
||||
if (morphLemmas != null) {
|
||||
final originalWrittenUses = morphLemmas.lemmasByPercent(
|
||||
filter: (use) =>
|
||||
use.useType == ConstructUseTypeEnum.wa ||
|
||||
use.useType == ConstructUseTypeEnum.ga,
|
||||
percent: 0.8,
|
||||
);
|
||||
|
||||
int numWordsTyped = 0;
|
||||
int numChoicesCorrect = 0;
|
||||
int numChoicesIncorrect = 0;
|
||||
for (final use in model.uses) {
|
||||
if (use.useType.summaryEnumType == AnalyticsSummaryEnum.numWordsTyped) {
|
||||
numWordsTyped++;
|
||||
} else if (use.useType.summaryEnumType ==
|
||||
AnalyticsSummaryEnum.numChoicesCorrect) {
|
||||
numChoicesCorrect++;
|
||||
} else if (use.useType.summaryEnumType ==
|
||||
AnalyticsSummaryEnum.numChoicesIncorrect) {
|
||||
numChoicesIncorrect++;
|
||||
correctSystemUseLemmas.addAll(originalWrittenUses.over);
|
||||
incorrectSystemUseLemmas.addAll(originalWrittenUses.under);
|
||||
|
||||
final systemGeneratedUses = morphLemmas.lemmasByPercent(
|
||||
filter: (use) =>
|
||||
use.useType != ConstructUseTypeEnum.wa &&
|
||||
use.useType != ConstructUseTypeEnum.ga &&
|
||||
use.useType != ConstructUseTypeEnum.unk &&
|
||||
use.pointValue != 0,
|
||||
percent: 0.8,
|
||||
);
|
||||
|
||||
correctSystemUseLemmas.addAll(systemGeneratedUses.over);
|
||||
incorrectSystemUseLemmas.addAll(systemGeneratedUses.under);
|
||||
}
|
||||
|
||||
final vocabLemmasCorrect = vocabLemmas?.lemmasByCorrectUse();
|
||||
|
||||
int? numWordsTyped;
|
||||
int? numChoicesCorrect;
|
||||
int? numChoicesIncorrect;
|
||||
if (model != null) {
|
||||
numWordsTyped = 0;
|
||||
numChoicesCorrect = 0;
|
||||
numChoicesIncorrect = 0;
|
||||
for (final use in model.uses) {
|
||||
if (use.useType.summaryEnumType == AnalyticsSummaryEnum.numWordsTyped) {
|
||||
numWordsTyped = numWordsTyped! + 1;
|
||||
} else if (use.useType.summaryEnumType ==
|
||||
AnalyticsSummaryEnum.numChoicesCorrect) {
|
||||
numChoicesCorrect = numChoicesCorrect! + 1;
|
||||
} else if (use.useType.summaryEnumType ==
|
||||
AnalyticsSummaryEnum.numChoicesIncorrect) {
|
||||
numChoicesIncorrect = numChoicesIncorrect! + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final numMessageSent = model.uses
|
||||
final numMessageSent = model?.uses
|
||||
.where((use) => use.useType.sentByUser)
|
||||
.map((use) => use.metadata.eventId)
|
||||
.toSet()
|
||||
|
|
@ -113,37 +161,41 @@ class AnalyticsSummaryModel {
|
|||
|
||||
return AnalyticsSummaryModel(
|
||||
username: userID,
|
||||
level: model.level,
|
||||
totalXP: model.totalXP,
|
||||
numLemmas: model.vocabLemmas,
|
||||
numLemmasUsedCorrectly: vocabLemmasCorrect.over.length,
|
||||
numLemmasUsedIncorrectly: vocabLemmasCorrect.under.length,
|
||||
numLemmasSmallXP: vocabLemmas.thresholdedLemmas(start: 0, end: 30).length,
|
||||
dataAvailable: model != null,
|
||||
level: model?.level,
|
||||
totalXP: model?.totalXP,
|
||||
numLemmas: model?.vocabLemmas,
|
||||
numLemmasUsedCorrectly: vocabLemmasCorrect?.over.length,
|
||||
numLemmasUsedIncorrectly: vocabLemmasCorrect?.under.length,
|
||||
numLemmasSmallXP:
|
||||
vocabLemmas?.thresholdedLemmas(start: 0, end: 30).length,
|
||||
numLemmasMediumXP:
|
||||
vocabLemmas.thresholdedLemmas(start: 31, end: 200).length,
|
||||
numLemmasLargeXP: vocabLemmas.thresholdedLemmas(start: 201).length,
|
||||
numMorphConstructs: model.grammarLemmas,
|
||||
listMorphConstructs: morphLemmas.lemmasToUses.entries
|
||||
vocabLemmas?.thresholdedLemmas(start: 31, end: 200).length,
|
||||
numLemmasLargeXP: vocabLemmas?.thresholdedLemmas(start: 201).length,
|
||||
numMorphConstructs: model?.grammarLemmas,
|
||||
listMorphConstructs: morphLemmas?.lemmasToUses.entries
|
||||
.map((entry) => getCopy(entry.value.first))
|
||||
.toList(),
|
||||
listMorphConstructsUsedCorrectly: morphLemmasPercentCorrect.over,
|
||||
listMorphConstructsUsedIncorrectly: morphLemmasPercentCorrect.under,
|
||||
listMorphSmallXP: morphLemmas.thresholdedLemmas(
|
||||
listMorphConstructsUsedCorrectlyOriginal: correctOriginalUseLemmas,
|
||||
listMorphConstructsUsedIncorrectlyOriginal: incorrectOriginalUseLemmas,
|
||||
listMorphConstructsUsedCorrectlySystem: correctSystemUseLemmas,
|
||||
listMorphConstructsUsedIncorrectlySystem: incorrectSystemUseLemmas,
|
||||
listMorphSmallXP: morphLemmas?.thresholdedLemmas(
|
||||
start: 0,
|
||||
end: 30,
|
||||
getCopy: getCopy,
|
||||
),
|
||||
listMorphMediumXP: morphLemmas.thresholdedLemmas(
|
||||
listMorphMediumXP: morphLemmas?.thresholdedLemmas(
|
||||
start: 31,
|
||||
end: 200,
|
||||
getCopy: getCopy,
|
||||
),
|
||||
listMorphLargeXP: morphLemmas.thresholdedLemmas(
|
||||
listMorphLargeXP: morphLemmas?.thresholdedLemmas(
|
||||
start: 201,
|
||||
end: 500,
|
||||
getCopy: getCopy,
|
||||
),
|
||||
listMorphHugeXP: morphLemmas.thresholdedLemmas(
|
||||
listMorphHugeXP: morphLemmas?.thresholdedLemmas(
|
||||
start: 501,
|
||||
getCopy: getCopy,
|
||||
),
|
||||
|
|
@ -154,10 +206,14 @@ class AnalyticsSummaryModel {
|
|||
);
|
||||
}
|
||||
|
||||
dynamic getValue(AnalyticsSummaryEnum key) {
|
||||
dynamic getValue(AnalyticsSummaryEnum key, BuildContext context) {
|
||||
switch (key) {
|
||||
case AnalyticsSummaryEnum.username:
|
||||
return username;
|
||||
case AnalyticsSummaryEnum.dataAvailable:
|
||||
return dataAvailable
|
||||
? L10n.of(context).available
|
||||
: L10n.of(context).unavailable;
|
||||
case AnalyticsSummaryEnum.level:
|
||||
return level;
|
||||
case AnalyticsSummaryEnum.totalXP:
|
||||
|
|
@ -178,10 +234,14 @@ class AnalyticsSummaryModel {
|
|||
return numMorphConstructs;
|
||||
case AnalyticsSummaryEnum.listMorphConstructs:
|
||||
return listMorphConstructs;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectly:
|
||||
return listMorphConstructsUsedCorrectly;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectly:
|
||||
return listMorphConstructsUsedIncorrectly;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlyOriginal:
|
||||
return listMorphConstructsUsedCorrectlyOriginal;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlyOriginal:
|
||||
return listMorphConstructsUsedIncorrectlyOriginal;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedCorrectlySystem:
|
||||
return listMorphConstructsUsedCorrectlySystem;
|
||||
case AnalyticsSummaryEnum.listMorphConstructsUsedIncorrectlySystem:
|
||||
return listMorphConstructsUsedIncorrectlySystem;
|
||||
case AnalyticsSummaryEnum.listMorphSmallXP:
|
||||
return listMorphSmallXP;
|
||||
case AnalyticsSummaryEnum.listMorphMediumXP:
|
||||
|
|
@ -214,8 +274,10 @@ class AnalyticsSummaryModel {
|
|||
'numLemmasLargeXP': numLemmasLargeXP,
|
||||
'numMorphConstructs': numMorphConstructs,
|
||||
'listMorphConstructs': listMorphConstructs,
|
||||
'listMorphConstructsUsedCorrectly': listMorphConstructsUsedCorrectly,
|
||||
'listMorphConstructsUsedIncorrectly': listMorphConstructsUsedIncorrectly,
|
||||
'listMorphConstructsUsedCorrectly':
|
||||
listMorphConstructsUsedCorrectlyOriginal,
|
||||
'listMorphConstructsUsedIncorrectly':
|
||||
listMorphConstructsUsedIncorrectlyOriginal,
|
||||
'listMorphSmallXP': listMorphSmallXP,
|
||||
'listMorphMediumXP': listMorphMediumXP,
|
||||
'listMorphLargeXP': listMorphLargeXP,
|
||||
|
|
|
|||
|
|
@ -247,8 +247,54 @@ class LemmasToUsesWrapper {
|
|||
|
||||
LemmasToUsesWrapper(this.lemmasToUses);
|
||||
|
||||
Map<String, List<OneConstructUse>> lemmasToFilteredUses(
|
||||
bool Function(OneConstructUse) filter,
|
||||
) {
|
||||
final Map<String, List<OneConstructUse>> lemmasToOneConstructUses = {};
|
||||
for (final entry in lemmasToUses.entries) {
|
||||
final lemma = entry.key;
|
||||
final uses = entry.value;
|
||||
lemmasToOneConstructUses[lemma] =
|
||||
uses.expand((use) => use.uses).toList().where(filter).toList();
|
||||
}
|
||||
return lemmasToOneConstructUses;
|
||||
}
|
||||
|
||||
LemmasOverUnderList lemmasByPercent({
|
||||
required bool Function(OneConstructUse) filter,
|
||||
required double percent,
|
||||
}) {
|
||||
final List<String> correctUseLemmas = [];
|
||||
final List<String> incorrectUseLemmas = [];
|
||||
|
||||
final uses = lemmasToFilteredUses(filter);
|
||||
for (final entry in uses.entries) {
|
||||
final List<OneConstructUse> correctUses = [];
|
||||
final List<OneConstructUse> incorrectUses = [];
|
||||
|
||||
final lemma = entry.key;
|
||||
final uses = entry.value.toList();
|
||||
|
||||
for (final use in uses) {
|
||||
use.pointValue > 0 ? correctUses.add(use) : incorrectUses.add(use);
|
||||
}
|
||||
|
||||
final totalUses = correctUses.length + incorrectUses.length;
|
||||
final percent = totalUses == 0 ? 0 : correctUses.length / totalUses;
|
||||
|
||||
percent > 0.8
|
||||
? correctUseLemmas.add(lemma)
|
||||
: incorrectUseLemmas.add(lemma);
|
||||
}
|
||||
|
||||
return LemmasOverUnderList(
|
||||
over: correctUseLemmas,
|
||||
under: incorrectUseLemmas,
|
||||
);
|
||||
}
|
||||
|
||||
/// Return an object containing two lists, one of lemmas with
|
||||
/// any correct uses and one of lemmas with any incorrect uses
|
||||
/// any correct uses and one of lemmas no correct uses
|
||||
LemmasOverUnderList lemmasByCorrectUse({
|
||||
String Function(ConstructUses)? getCopy,
|
||||
}) {
|
||||
|
|
@ -260,46 +306,13 @@ class LemmasToUsesWrapper {
|
|||
final copy = getCopy?.call(constructUses.first) ?? lemma;
|
||||
if (constructUses.any((use) => use.hasCorrectUse)) {
|
||||
correctLemmas.add(copy);
|
||||
}
|
||||
if (constructUses.any((use) => use.hasIncorrectUse)) {
|
||||
} else {
|
||||
incorrectLemmas.add(copy);
|
||||
}
|
||||
}
|
||||
return LemmasOverUnderList(over: correctLemmas, under: incorrectLemmas);
|
||||
}
|
||||
|
||||
/// Return an object containing two lists, one of lemmas with percent used
|
||||
/// correctly > percent and one of lemmas with percent used correctly < percent
|
||||
LemmasOverUnderList lemmasByPercent({
|
||||
double percent = 0.8,
|
||||
String Function(ConstructUses)? getCopy,
|
||||
}) {
|
||||
final List<String> overLemmas = [];
|
||||
final List<String> underLemmas = [];
|
||||
for (final entry in lemmasToUses.entries) {
|
||||
final lemma = entry.key;
|
||||
final constructUses = entry.value;
|
||||
final uses = constructUses.map((u) => u.uses).expand((e) => e).toList();
|
||||
|
||||
int correct = 0;
|
||||
int incorrect = 0;
|
||||
for (final use in uses) {
|
||||
if (use.pointValue > 0) {
|
||||
correct++;
|
||||
} else if (use.pointValue < 0) {
|
||||
incorrect++;
|
||||
}
|
||||
}
|
||||
|
||||
if (correct + incorrect == 0) continue;
|
||||
|
||||
final copy = getCopy?.call(constructUses.first) ?? lemma;
|
||||
final percent = correct / (correct + incorrect);
|
||||
percent >= percent ? overLemmas.add(copy) : underLemmas.add(copy);
|
||||
}
|
||||
return LemmasOverUnderList(over: overLemmas, under: underLemmas);
|
||||
}
|
||||
|
||||
int totalXP(String lemma) {
|
||||
final uses = lemmasToUses[lemma];
|
||||
if (uses == null) return 0;
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ClassInvitationSelection extends StatefulWidget {
|
||||
const ClassInvitationSelection({super.key});
|
||||
|
||||
@override
|
||||
ClassInvitationSelectionController createState() =>
|
||||
ClassInvitationSelectionController();
|
||||
}
|
||||
|
||||
class ClassInvitationSelectionController
|
||||
extends State<ClassInvitationSelection> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
late String currentSearchTerm;
|
||||
bool loading = true;
|
||||
List<User> allClassParticipants = [];
|
||||
List<User> allChatParticipants = [];
|
||||
List<User> classParticipantsFilteredByChat = [];
|
||||
|
||||
///Class participants filtered by chat participants and any search query
|
||||
List<User> foundProfiles = [];
|
||||
|
||||
Timer? coolDown;
|
||||
|
||||
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
|
||||
StreamSubscription<SyncUpdate>? _spaceSubscription;
|
||||
|
||||
void inviteAction(BuildContext context, String id) async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!)!;
|
||||
if (OkCancelResult.ok !=
|
||||
await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).inviteContactToGroup(room.name),
|
||||
okLabel: L10n.of(context).yes,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.invite(id),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
// #Pangea
|
||||
// content: Text(L10n.of(context).contactHasBeenInvitedToTheGroup),
|
||||
content: Text(L10n.of(context).contactHasBeenInvitedToTheChat),
|
||||
// Pangea#
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setDisplayListWithCoolDown(String text) {
|
||||
coolDown?.cancel();
|
||||
coolDown = Timer(
|
||||
const Duration(milliseconds: 0),
|
||||
() => _setfoundProfiles(context, text),
|
||||
);
|
||||
}
|
||||
|
||||
void _setfoundProfiles(BuildContext context, String text) {
|
||||
coolDown?.cancel();
|
||||
// debugger(when: kDebugMode);
|
||||
allClassParticipants = getClassParticipants(context);
|
||||
allChatParticipants = getChatParticipants(context);
|
||||
classParticipantsFilteredByChat = getClassParticipantsFilteredByChat();
|
||||
|
||||
currentSearchTerm = text;
|
||||
|
||||
foundProfiles = currentSearchTerm.isNotEmpty
|
||||
? classParticipantsFilteredByChat
|
||||
.where(
|
||||
(user) =>
|
||||
user.displayName?.contains(text) ??
|
||||
false || user.id.contains(text),
|
||||
)
|
||||
.toList()
|
||||
: classParticipantsFilteredByChat;
|
||||
|
||||
setState(() => loading = false);
|
||||
}
|
||||
|
||||
Room? _getParentClass(BuildContext context) => Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
(r) => r.isSpace,
|
||||
)
|
||||
.firstWhereOrNull(
|
||||
(space) => space.spaceChildren.any(
|
||||
(ithroom) => ithroom.roomId == roomId,
|
||||
),
|
||||
);
|
||||
|
||||
List<User> getClassParticipants(BuildContext context) {
|
||||
final Room? parent = _getParentClass(context);
|
||||
if (parent == null) return [];
|
||||
|
||||
final List<User> classParticipants =
|
||||
parent.getParticipants([Membership.join]);
|
||||
return classParticipants;
|
||||
}
|
||||
|
||||
List<User> getChatParticipants(BuildContext context) => Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(roomId!)!
|
||||
.getParticipants([Membership.join, Membership.invite]).toList();
|
||||
|
||||
List<User> getClassParticipantsFilteredByChat() => allClassParticipants
|
||||
.where(
|
||||
(profile) =>
|
||||
allChatParticipants.indexWhere((u) => u.id == profile.id) == -1,
|
||||
)
|
||||
.toList();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final Room? classParent = _getParentClass(context);
|
||||
await classParent
|
||||
?.requestParticipants([Membership.join, Membership.invite]);
|
||||
_setfoundProfiles(context, "");
|
||||
_spaceSubscription = Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(event) =>
|
||||
event.rooms?.join?.keys
|
||||
.any((ithRoomId) => ithRoomId == classParent?.id) ??
|
||||
false,
|
||||
)
|
||||
.listen(
|
||||
(SyncUpdate syncUpdate) async {
|
||||
debugPrint("updating lists");
|
||||
await classParent
|
||||
?.requestParticipants([Membership.join, Membership.invite]);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_spaceSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// Widget build(BuildContext context) => InvitationSelectionView(this);
|
||||
Widget build(BuildContext context) => const SizedBox();
|
||||
}
|
||||
|
|
@ -70,9 +70,6 @@ void chatListHandleSpaceTap(
|
|||
if (await space.leaveIfFull()) {
|
||||
throw L10n.of(context).roomFull;
|
||||
}
|
||||
if (space.isSpace) {
|
||||
space.joinAnalyticsRoomsInSpace();
|
||||
}
|
||||
setActiveSpaceAndCloseChat();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ Future<List<SpaceTeacher>> getReportTeachers(
|
|||
|
||||
final List<Room> otherSpaces = Matrix.of(context)
|
||||
.client
|
||||
.spacesImIn
|
||||
.where((space) => !reportRoomParentSpaces.contains(space))
|
||||
.rooms
|
||||
.where((room) => room.isSpace && !reportRoomParentSpaces.contains(room))
|
||||
.toList();
|
||||
|
||||
for (final space in otherSpaces) {
|
||||
|
|
|
|||
|
|
@ -33,21 +33,30 @@ class DownloadAnalyticsDialog extends StatefulWidget {
|
|||
|
||||
class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
||||
bool _initialized = false;
|
||||
bool _loading = false;
|
||||
bool _finishedDownload = false;
|
||||
bool _downloaded = false;
|
||||
bool _joiningRooms = false;
|
||||
bool _downloading = false;
|
||||
|
||||
bool get _loading => _joiningRooms || _downloading || !_initialized;
|
||||
|
||||
String? _error;
|
||||
|
||||
Map<String, int> _downloadStatues = {};
|
||||
Map<String, int> _downloadStatuses = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.space
|
||||
.requestParticipants([Membership.join], false, true).whenComplete(() {
|
||||
_resetDownloadStatuses();
|
||||
_initialized = true;
|
||||
if (mounted) setState(() {});
|
||||
}).catchError((e, s) {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
await widget.space.requestParticipants(
|
||||
[Membership.join],
|
||||
false,
|
||||
true,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -55,9 +64,26 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
"spaceID": widget.space.id,
|
||||
},
|
||||
);
|
||||
if (mounted) setState(() => _error = e.toString());
|
||||
return <User>[];
|
||||
});
|
||||
} finally {
|
||||
if (mounted) setState(() => _initialized = true);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadType _downloadType = DownloadType.csv;
|
||||
|
||||
void _setDownloadType(DownloadType type) {
|
||||
_clean();
|
||||
if (mounted) setState(() => _downloadType = type);
|
||||
}
|
||||
|
||||
void _clean() {
|
||||
_error = null;
|
||||
_joiningRooms = false;
|
||||
_downloading = false;
|
||||
_downloaded = false;
|
||||
_downloadStatuses = Map.fromEntries(
|
||||
_usersToDownload.map((user) => MapEntry(user.id, 0)),
|
||||
);
|
||||
}
|
||||
|
||||
List<User> get _usersToDownload => widget.space
|
||||
|
|
@ -66,13 +92,20 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
.toList();
|
||||
|
||||
Color _downloadStatusColor(String userID) {
|
||||
final status = _downloadStatues[userID];
|
||||
final status = _downloadStatuses[userID];
|
||||
if (status == 1) return Colors.yellow;
|
||||
if (status == 2) return Colors.green;
|
||||
if (status == -1) return Colors.red;
|
||||
if ((status ?? 0) < 0) return Colors.red;
|
||||
return Colors.grey;
|
||||
}
|
||||
|
||||
String? get _statusText {
|
||||
if (_joiningRooms) return L10n.of(context).accessingMemberAnalytics;
|
||||
if (_downloading) return L10n.of(context).downloading;
|
||||
if (_downloaded) return L10n.of(context).downloadComplete;
|
||||
return null;
|
||||
}
|
||||
|
||||
Room? _userAnalyticsRoom(String userID) {
|
||||
final rooms = widget.space.client.rooms;
|
||||
final l2 = MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
|
@ -84,12 +117,27 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
|
||||
Future<void> _runDownload() async {
|
||||
try {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
_resetDownloadStatuses();
|
||||
if (mounted) setState(() {});
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = null;
|
||||
_joiningRooms = true;
|
||||
});
|
||||
|
||||
await widget.space.joinAnalyticsRooms();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_joiningRooms = false;
|
||||
_downloading = true;
|
||||
});
|
||||
}
|
||||
|
||||
await _downloadSpaceAnalytics();
|
||||
if (mounted) setState(() => _finishedDownload = true);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_downloading = false;
|
||||
_downloaded = true;
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
|
|
@ -98,11 +146,10 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
"spaceID": widget.space.id,
|
||||
},
|
||||
);
|
||||
_resetDownloadStatuses();
|
||||
|
||||
_clean();
|
||||
_error = e.toString();
|
||||
if (mounted) setState(() {});
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,14 +178,17 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
|
||||
Future<AnalyticsSummaryModel?> _getUserAnalyticsModel(String userID) async {
|
||||
try {
|
||||
setState(() => _downloadStatues[userID] = 1);
|
||||
final userAnalyticsRoom = _userAnalyticsRoom(userID);
|
||||
_downloadStatuses[userID] = userAnalyticsRoom != null ? 1 : -1;
|
||||
if (mounted) setState(() {});
|
||||
|
||||
final constructEvents = await userAnalyticsRoom?.getAnalyticsEvents(
|
||||
userId: userID,
|
||||
);
|
||||
|
||||
if (constructEvents == null) {
|
||||
setState(() => _downloadStatues[userID] = 0);
|
||||
return null;
|
||||
if (mounted) setState(() => _downloadStatuses[userID] = -1);
|
||||
return AnalyticsSummaryModel.emptyModel(userID);
|
||||
}
|
||||
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
|
@ -148,12 +198,12 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
|
||||
final constructs = ConstructListModel(uses: uses);
|
||||
final summary = AnalyticsSummaryModel.fromConstructListModel(
|
||||
constructs,
|
||||
userID,
|
||||
constructs,
|
||||
getCopy,
|
||||
context,
|
||||
);
|
||||
setState(() => _downloadStatues[userID] = 2);
|
||||
if (mounted) setState(() => _downloadStatuses[userID] = 2);
|
||||
return summary;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -164,7 +214,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
"userID": userID,
|
||||
},
|
||||
);
|
||||
setState(() => _downloadStatues[userID] = -1);
|
||||
if (mounted) setState(() => _downloadStatuses[userID] = -2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -175,7 +225,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
final List<CellValue> row = [];
|
||||
for (int i = 0; i < AnalyticsSummaryEnum.values.length; i++) {
|
||||
final key = AnalyticsSummaryEnum.values[i];
|
||||
final value = summary.getValue(key);
|
||||
final value = summary.getValue(key, context);
|
||||
if (value is int) {
|
||||
row.add(IntCellValue(value));
|
||||
} else if (value is String) {
|
||||
|
|
@ -232,7 +282,8 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
final row = [];
|
||||
for (int i = 0; i < AnalyticsSummaryEnum.values.length; i++) {
|
||||
final key = AnalyticsSummaryEnum.values[i];
|
||||
final value = summary.getValue(key);
|
||||
final value = summary.getValue(key, context);
|
||||
if (value == null) continue;
|
||||
value is List<String> ? row.add(value.join(", ")) : row.add(value);
|
||||
}
|
||||
rows.add(row);
|
||||
|
|
@ -251,21 +302,6 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
use.lemma;
|
||||
}
|
||||
|
||||
DownloadType _downloadType = DownloadType.csv;
|
||||
|
||||
void _setDownloadType(DownloadType type) {
|
||||
_resetDownloadStatuses();
|
||||
setState(() => _downloadType = type);
|
||||
}
|
||||
|
||||
void _resetDownloadStatuses() {
|
||||
_error = null;
|
||||
_finishedDownload = false;
|
||||
_downloadStatues = Map.fromEntries(
|
||||
_usersToDownload.map((user) => MapEntry(user.id, 0)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
|
|
@ -312,28 +348,26 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
itemCount: _usersToDownload.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = _usersToDownload[index];
|
||||
final analyticsAvailable =
|
||||
_userAnalyticsRoom(user.id) != null;
|
||||
|
||||
String tooltip = "";
|
||||
if (!analyticsAvailable) {
|
||||
if (_downloadStatuses[user.id] == -1) {
|
||||
tooltip = L10n.of(context).analyticsNotAvailable;
|
||||
} else if (_downloadStatues[user.id] == -1) {
|
||||
} else if (_downloadStatuses[user.id] == -2) {
|
||||
tooltip = L10n.of(context).failedFetchUserAnalytics;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Opacity(
|
||||
opacity: analyticsAvailable &&
|
||||
_downloadStatues[user.id] != -1
|
||||
? 1
|
||||
: 0.5,
|
||||
child: AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity:
|
||||
(_downloadStatuses[user.id] ?? 0) > 0 ? 1 : 0.5,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: !analyticsAvailable
|
||||
width: 40,
|
||||
height: 30,
|
||||
child: (_downloadStatuses[user.id] ?? 0) < 0
|
||||
? const Icon(
|
||||
Icons.error_outline,
|
||||
size: 16,
|
||||
|
|
@ -377,7 +411,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
padding: const EdgeInsets.fromLTRB(8.0, 16.0, 8.0, 8.0),
|
||||
child: OutlinedButton(
|
||||
onPressed: _loading || !_initialized ? null : _runDownload,
|
||||
child: _initialized
|
||||
child: _initialized && !_loading
|
||||
? Text(
|
||||
_loading
|
||||
? L10n.of(context).downloading
|
||||
|
|
@ -385,17 +419,17 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
)
|
||||
: const SizedBox(
|
||||
height: 10,
|
||||
width: 10,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _finishedDownload
|
||||
child: _statusText != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context).downloadComplete),
|
||||
child: Text(_statusText!),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ abstract class ClientManager {
|
|||
PangeaEventTypes.capacity,
|
||||
EventTypes.RoomPowerLevels,
|
||||
PangeaEventTypes.userChosenEmoji,
|
||||
EventTypes.RoomJoinRules,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue