move student summary analytics events to analytics rooms
This commit is contained in:
parent
ffe49bd748
commit
12e364a32d
32 changed files with 2036 additions and 1631 deletions
|
|
@ -22,7 +22,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
|
|||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
|
@ -643,34 +642,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(
|
||||
eventId: msgEventId,
|
||||
chatId: room.id,
|
||||
useType: useType ?? UseType.un,
|
||||
time: DateTime.now(),
|
||||
),
|
||||
isEdit: previousEdit != null,
|
||||
);
|
||||
|
||||
if (choreo != null &&
|
||||
tokensSent != null &&
|
||||
originalSent?.langCode ==
|
||||
pangeaController.languageController
|
||||
.activeL2Code(roomID: room.id)) {
|
||||
pangeaController.myAnalytics.saveConstructsMixed(
|
||||
[
|
||||
// ...choreo.toVocabUse(tokensSent.tokens, room.id, msgEventId),
|
||||
...choreo.toGrammarConstructUse(msgEventId, room.id),
|
||||
],
|
||||
originalSent!.langCode,
|
||||
isEdit: previousEdit != null,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (err, stack) => ErrorHandler.logError(e: err, s: stack),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/new_space/new_space_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
|
|
@ -77,7 +76,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
stateKey: '',
|
||||
content: {
|
||||
'events': {
|
||||
PangeaEventTypes.studentAnalyticsSummary: 0,
|
||||
EventTypes.spaceChild: 0,
|
||||
},
|
||||
'users_default': 0,
|
||||
|
|
|
|||
|
|
@ -109,4 +109,7 @@ class ModelKey {
|
|||
"discussion_trigger_reaction_enabled";
|
||||
static const String discussionTriggerReactionKey =
|
||||
"discussion_trigger_reaction_key";
|
||||
|
||||
static const String prevEventId = "prev_event_id";
|
||||
static const String prevLastUpdated = "prev_last_updated";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ class PangeaEventTypes {
|
|||
|
||||
static const rules = "p.rules";
|
||||
|
||||
static const studentAnalyticsSummary = "pangea.usranalytics";
|
||||
// static const studentAnalyticsSummary = "pangea.usranalytics";
|
||||
static const summaryAnalytics = "pangea.summaryAnalytics";
|
||||
static const construct = "pangea.construct";
|
||||
|
||||
static const translation = "pangea.translation";
|
||||
static const tokens = "pangea.tokens";
|
||||
static const choreoRecord = "pangea.record";
|
||||
static const representation = "pangea.representation";
|
||||
|
||||
static const vocab = "p.vocab";
|
||||
// static const vocab = "p.vocab";
|
||||
static const roomInfo = "pangea.roomtopic";
|
||||
|
||||
static const audio = "p.audio";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import 'dart:developer';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -13,16 +15,14 @@ import 'package:matrix/matrix.dart';
|
|||
import '../constants/class_default_values.dart';
|
||||
import '../extensions/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension.dart';
|
||||
import '../matrix_event_wrappers/construct_analytics_event.dart';
|
||||
import '../models/chart_analytics_model.dart';
|
||||
import '../models/student_analytics_event.dart';
|
||||
import 'base_controller.dart';
|
||||
import 'pangea_controller.dart';
|
||||
|
||||
class AnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
final List<CacheModel> _cachedModels = [];
|
||||
final List<AnalyticsCacheModel> _cachedAnalyticsModels = [];
|
||||
final List<ConstructCacheEntry> _cachedConstructs = [];
|
||||
|
||||
AnalyticsController(PangeaController pangeaController) : super() {
|
||||
|
|
@ -57,272 +57,384 @@ class AnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<ChartAnalyticsModel?>> allClassAnalytics() async {
|
||||
final List<Future<ChartAnalyticsModel?>> classAnalyticFutures = [];
|
||||
for (final classRoom in (await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImTeaching)) {
|
||||
classAnalyticFutures.add(
|
||||
getAnalytics(classRoom: classRoom),
|
||||
);
|
||||
Future<List<SummaryAnalyticsEvent>> allMySummaryAnalytics() async {
|
||||
final analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
final List<SummaryAnalyticsEvent> allEvents = [];
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<SummaryAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
))
|
||||
?.cast<SummaryAnalyticsEvent>();
|
||||
allEvents.addAll(roomEvents ?? []);
|
||||
}
|
||||
|
||||
return Future.wait(classAnalyticFutures);
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
ChartAnalyticsModel? getAnalyticsLocal({
|
||||
TimeSpan? timeSpan,
|
||||
String? classId,
|
||||
String? studentId,
|
||||
String? chatId,
|
||||
bool forceUpdate = false,
|
||||
bool updateExpired = false,
|
||||
}) {
|
||||
timeSpan ??= currentAnalyticsTimeSpan;
|
||||
final int index = _cachedModels.indexWhere(
|
||||
(e) =>
|
||||
(e.timeSpan == timeSpan) &&
|
||||
(e.classId == classId) &&
|
||||
(e.studentId == studentId) &&
|
||||
(e.chatId == chatId),
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
if ((updateExpired && _cachedModels[index].isExpired) || forceUpdate) {
|
||||
_cachedModels.removeAt(index);
|
||||
} else {
|
||||
return _cachedModels[index].chartAnalyticsModel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ChartAnalyticsModel> getAnalytics({
|
||||
TimeSpan? timeSpan,
|
||||
Room? classRoom,
|
||||
String? studentId,
|
||||
String? chatId,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
timeSpan ??= currentAnalyticsTimeSpan;
|
||||
try {
|
||||
final cachedModel = getAnalyticsLocal(
|
||||
classId: classRoom?.id,
|
||||
studentId: studentId,
|
||||
chatId: chatId,
|
||||
updateExpired: true,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
if (cachedModel != null) return cachedModel;
|
||||
// debugger(when: classRoom?.displayname.contains('clizass') ?? false);
|
||||
late List<StudentAnalyticsEvent?> studentAnalyticsSummaryEvents;
|
||||
if (classRoom == null) {
|
||||
if (studentId == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "studentId should have been defined",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
} else {
|
||||
studentAnalyticsSummaryEvents =
|
||||
await _pangeaController.myAnalytics.allMyAnalyticsEvents();
|
||||
}
|
||||
} else {
|
||||
if (studentId != null) {
|
||||
studentAnalyticsSummaryEvents = [
|
||||
await classRoom.getStudentAnalytics(studentId),
|
||||
];
|
||||
} else {
|
||||
studentAnalyticsSummaryEvents = await classRoom.getClassAnalytics();
|
||||
}
|
||||
if (studentId != null && chatId != null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "studentId and chatId should have both been defined",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
studentAnalyticsSummaryEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
final List<RecentMessageRecord> msgs = [];
|
||||
for (final event in studentAnalyticsSummaryEvents) {
|
||||
if (event != null) {
|
||||
msgs.addAll(event.content.messages);
|
||||
} else {
|
||||
debugPrint("studentAnalyticsSummaryEvent is null");
|
||||
}
|
||||
}
|
||||
|
||||
final newModel = ChartAnalyticsModel(
|
||||
timeSpan: timeSpan,
|
||||
msgs: msgs,
|
||||
chatId: chatId,
|
||||
);
|
||||
|
||||
_cachedModels.add(
|
||||
CacheModel(
|
||||
timeSpan: timeSpan,
|
||||
classId: classRoom?.id,
|
||||
studentId: studentId,
|
||||
chatId: chatId,
|
||||
chartAnalyticsModel: newModel,
|
||||
),
|
||||
);
|
||||
|
||||
return newModel;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan, chatId: chatId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChartAnalyticsModel> getAnalyticsForPrivateChats({
|
||||
TimeSpan? timeSpan,
|
||||
required Room? classRoom,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
timeSpan ??= currentAnalyticsTimeSpan;
|
||||
|
||||
try {
|
||||
if (classRoom == null) {
|
||||
return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan);
|
||||
}
|
||||
|
||||
final cachedModel = getAnalyticsLocal(
|
||||
classId: classRoom.id,
|
||||
studentId: null,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
updateExpired: true,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
if (cachedModel != null) return cachedModel;
|
||||
final List<StudentAnalyticsEvent?> studentAnalyticsSummaryEvents =
|
||||
await classRoom.getClassAnalytics();
|
||||
final List<String> directChatIds =
|
||||
classRoom.childrenAndGrandChildrenDirectChatIds;
|
||||
|
||||
final List<RecentMessageRecord> msgs = [];
|
||||
for (final event in studentAnalyticsSummaryEvents) {
|
||||
if (event != null) {
|
||||
msgs.addAll(
|
||||
event.content.messages
|
||||
.where((m) => directChatIds.contains(m.chatId)),
|
||||
);
|
||||
} else {
|
||||
debugPrint("studentAnalyticsSummaryEvent is null");
|
||||
}
|
||||
}
|
||||
final newModel = ChartAnalyticsModel(
|
||||
timeSpan: timeSpan,
|
||||
msgs: msgs,
|
||||
chatId: null,
|
||||
);
|
||||
|
||||
_cachedModels.add(
|
||||
CacheModel(
|
||||
timeSpan: timeSpan,
|
||||
classId: classRoom.id,
|
||||
studentId: null,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
chartAnalyticsModel: newModel,
|
||||
),
|
||||
);
|
||||
|
||||
return newModel;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
List<ConstructEvent>? _constructs;
|
||||
bool settingConstructs = false;
|
||||
|
||||
List<ConstructEvent>? get constructs => _constructs;
|
||||
|
||||
String? getLangCode({
|
||||
Room? space,
|
||||
String? roomID,
|
||||
}) {
|
||||
final String? targetRoomID = space?.id ?? roomID;
|
||||
final String? roomLangCode =
|
||||
_pangeaController.languageController.activeL2Code(roomID: targetRoomID);
|
||||
final String? userLangCode =
|
||||
_pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
return roomLangCode ?? userLangCode;
|
||||
}
|
||||
|
||||
Future<Room> myAnalyticsRoom(String langCode) =>
|
||||
_pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
|
||||
Room? studentAnalyticsRoom(String studentId, String langCode) =>
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(
|
||||
langCode,
|
||||
studentId,
|
||||
);
|
||||
|
||||
Future<List<ConstructEvent>> allMyConstructs(
|
||||
String langCode, {
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
final Room analyticsRoom = await myAnalyticsRoom(langCode);
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
|
||||
final allConstructs = type == null
|
||||
? await analyticsRoom.allConstructEvents
|
||||
: (await analyticsRoom.allConstructEvents)
|
||||
.where((e) => e.content.type == type)
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < allConstructs.length; i++) {
|
||||
final construct = allConstructs[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere((u) => adminSpaceRooms.contains(u.chatId));
|
||||
}
|
||||
|
||||
return allConstructs
|
||||
.where((construct) => construct.content.uses.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> allSpaceMemberConstructs(
|
||||
Future<List<SummaryAnalyticsEvent>> allSpaceMemberAnalytics(
|
||||
Room space,
|
||||
String langCode, {
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
final List<Future<List<ConstructEvent>>> constructEventFutures = [];
|
||||
) async {
|
||||
final langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
final List<SummaryAnalyticsEvent> analyticsEvents = [];
|
||||
await space.postLoad();
|
||||
await space.requestParticipants();
|
||||
for (final student in space.students) {
|
||||
final Room? room = _pangeaController.matrixState.client
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (room != null) constructEventFutures.add(room.allConstructEvents);
|
||||
if (analyticsRoom != null) {
|
||||
final List<SummaryAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
))
|
||||
?.cast<SummaryAnalyticsEvent>();
|
||||
analyticsEvents.addAll(roomEvents ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
final List<List<ConstructEvent>> constructLists =
|
||||
await Future.wait(constructEventFutures);
|
||||
|
||||
final List<String> spaceChildrenIds = space.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
final List<ConstructEvent> allConstructs = [];
|
||||
for (final constructList in constructLists) {
|
||||
for (int i = 0; i < constructList.length; i++) {
|
||||
final construct = constructList[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere((u) => !spaceChildrenIds.contains(u.chatId));
|
||||
}
|
||||
allConstructs.addAll(
|
||||
constructList.where((e) => e.content.uses.isNotEmpty),
|
||||
final List<SummaryAnalyticsEvent> allAnalyticsEvents = [];
|
||||
for (final analyticsEvent in analyticsEvents) {
|
||||
analyticsEvent.content.messages.removeWhere(
|
||||
(msg) => !spaceChildrenIds.contains(msg.chatId),
|
||||
);
|
||||
allAnalyticsEvents.add(analyticsEvent);
|
||||
}
|
||||
|
||||
return allAnalyticsEvents;
|
||||
}
|
||||
|
||||
ChartAnalyticsModel? getAnalyticsLocal({
|
||||
TimeSpan? timeSpan,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool forceUpdate = false,
|
||||
bool updateExpired = false,
|
||||
}) {
|
||||
timeSpan ??= currentAnalyticsTimeSpan;
|
||||
final int index = _cachedAnalyticsModels.indexWhere(
|
||||
(e) =>
|
||||
(e.timeSpan == timeSpan) &&
|
||||
(e.defaultSelected.id == defaultSelected.id) &&
|
||||
(e.defaultSelected.type == defaultSelected.type) &&
|
||||
(e.selected?.id == selected?.id) &&
|
||||
(e.selected?.type == selected?.type),
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
if ((updateExpired && _cachedAnalyticsModels[index].isExpired) ||
|
||||
forceUpdate) {
|
||||
_cachedAnalyticsModels.removeAt(index);
|
||||
} else {
|
||||
return _cachedAnalyticsModels[index].chartAnalyticsModel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void cacheAnalytics({
|
||||
required ChartAnalyticsModel chartAnalyticsModel,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
TimeSpan? timeSpan,
|
||||
}) {
|
||||
_cachedAnalyticsModels.add(
|
||||
AnalyticsCacheModel(
|
||||
timeSpan: timeSpan ?? currentAnalyticsTimeSpan,
|
||||
chartAnalyticsModel: chartAnalyticsModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterStudentAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String? studentId,
|
||||
) {
|
||||
final List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
filtered.removeWhere((e) => e.event.senderId != studentId);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterRoomAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String? roomID,
|
||||
) {
|
||||
List<SummaryAnalyticsEvent> filtered = [...unfiltered];
|
||||
filtered = filtered
|
||||
.where(
|
||||
(e) => (e.content).messages.any((u) => u.chatId == roomID),
|
||||
)
|
||||
.toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => u.chatId != roomID,
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterPrivateChatAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
Room? space,
|
||||
) {
|
||||
final List<String> directChatIds =
|
||||
space?.childrenAndGrandChildrenDirectChatIds ?? [];
|
||||
List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
filtered = filtered.where((e) {
|
||||
return (e.content).messages.any(
|
||||
(u) => directChatIds.contains(u.chatId),
|
||||
);
|
||||
}).toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => !directChatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterSpaceAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String spaceId,
|
||||
) {
|
||||
final selectedSpace =
|
||||
_pangeaController.matrixState.client.getRoomById(spaceId);
|
||||
final List<String> chatIds = selectedSpace?.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
filtered = filtered
|
||||
.where(
|
||||
(e) => (e.content).messages.any((u) => chatIds.contains(u.chatId)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => !chatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> filterAnalytics({
|
||||
required List<SummaryAnalyticsEvent> unfilteredAnalytics,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
switch (selected?.type) {
|
||||
case null:
|
||||
return unfilteredAnalytics;
|
||||
case AnalyticsEntryType.student:
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
throw Exception(
|
||||
"student filtering not available for default filter ${defaultSelected.type}",
|
||||
);
|
||||
}
|
||||
return filterStudentAnalytics(unfilteredAnalytics, selected?.id);
|
||||
case AnalyticsEntryType.room:
|
||||
return filterRoomAnalytics(unfilteredAnalytics, selected?.id);
|
||||
case AnalyticsEntryType.privateChats:
|
||||
if (defaultSelected.type == AnalyticsEntryType.student) {
|
||||
throw "private chat filtering not available for my analytics";
|
||||
}
|
||||
return filterPrivateChatAnalytics(unfilteredAnalytics, space);
|
||||
case AnalyticsEntryType.space:
|
||||
return filterSpaceAnalytics(unfilteredAnalytics, selected!.id);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChartAnalyticsModel> getAnalytics({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
try {
|
||||
final local = getAnalyticsLocal(
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
return local;
|
||||
}
|
||||
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
Room? space;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
);
|
||||
if (space == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "space not found in getAnalytics",
|
||||
data: {
|
||||
"defaultSelected": defaultSelected,
|
||||
"selected": selected,
|
||||
},
|
||||
);
|
||||
return ChartAnalyticsModel(
|
||||
msgs: [],
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final List<SummaryAnalyticsEvent> summaryEvents =
|
||||
defaultSelected.type == AnalyticsEntryType.space
|
||||
? await allSpaceMemberAnalytics(space!)
|
||||
: await allMySummaryAnalytics();
|
||||
|
||||
final List<SummaryAnalyticsEvent> filteredAnalytics =
|
||||
await filterAnalytics(
|
||||
unfilteredAnalytics: summaryEvents,
|
||||
defaultSelected: defaultSelected,
|
||||
space: space,
|
||||
selected: selected,
|
||||
);
|
||||
|
||||
final ChartAnalyticsModel newModel = ChartAnalyticsModel(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
msgs: filteredAnalytics
|
||||
.map((event) => event.content.messages)
|
||||
.expand((msgs) => msgs)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (local == null) {
|
||||
cacheAnalytics(
|
||||
chartAnalyticsModel: newModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
return newModel;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return ChartAnalyticsModel(
|
||||
msgs: [],
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////// CONSTRUCTS ////////////////////////////
|
||||
|
||||
List<ConstructAnalyticsEvent>? _constructs;
|
||||
bool settingConstructs = false;
|
||||
|
||||
List<ConstructAnalyticsEvent>? get constructs => _constructs;
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs({
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
List<ConstructAnalyticsEvent> allConstructs = [];
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
allConstructs.addAll(roomEvents ?? []);
|
||||
}
|
||||
|
||||
allConstructs = type == null
|
||||
? allConstructs
|
||||
: allConstructs.where((e) => e.content.type == type).toList();
|
||||
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
for (final construct in allConstructs) {
|
||||
final lemmaUses = construct.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere((u) => adminSpaceRooms.contains(u.chatId));
|
||||
}
|
||||
}
|
||||
|
||||
return allConstructs
|
||||
.where((construct) => construct.content.uses.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
|
||||
Room space, {
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
await space.postLoad();
|
||||
await space.requestParticipants();
|
||||
final String? langCode = _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
);
|
||||
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in allSpaceMemberConstructs",
|
||||
data: {
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom != null) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
constructEvents.addAll(roomEvents ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> spaceChildrenIds = space.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
final List<ConstructAnalyticsEvent> allConstructs = [];
|
||||
for (final constructEvent in constructEvents) {
|
||||
final lemmaUses = constructEvent.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere((u) => !spaceChildrenIds.contains(u.chatId));
|
||||
}
|
||||
|
||||
if (constructEvent.content.uses.isNotEmpty) {
|
||||
allConstructs.add(constructEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return type == null
|
||||
|
|
@ -330,51 +442,49 @@ class AnalyticsController extends BaseController {
|
|||
: allConstructs.where((e) => e.content.type == type).toList();
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterStudentConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
List<ConstructAnalyticsEvent> filterStudentConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
String? studentId,
|
||||
) {
|
||||
final List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered.removeWhere((e) => e.event.senderId != studentId);
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
filtered.removeWhere((element) => element.event.senderId != studentId);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterRoomConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
List<ConstructAnalyticsEvent> filterRoomConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
String? roomID,
|
||||
) {
|
||||
List<ConstructEvent> filtered = [...unfilteredConstructs];
|
||||
filtered = unfilteredConstructs
|
||||
.where((e) => e.content.uses.any((u) => u.chatId == roomID))
|
||||
.toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere((u) => u.chatId != roomID),
|
||||
);
|
||||
final List<ConstructAnalyticsEvent> filtered = [...unfilteredConstructs];
|
||||
for (final construct in filtered) {
|
||||
final lemmaUses = construct.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere((u) => u.chatId != roomID);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterPrivateChatConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
List<ConstructAnalyticsEvent> filterPrivateChatConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room parentSpace,
|
||||
) {
|
||||
final List<String> directChatIds =
|
||||
parentSpace.childrenAndGrandChildrenDirectChatIds;
|
||||
List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered = filtered.where((e) {
|
||||
return e.content.uses.any((u) => directChatIds.contains(u.chatId));
|
||||
}).toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere(
|
||||
(u) => !directChatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
for (final construct in filtered) {
|
||||
final lemmaUses = construct.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere((u) => !directChatIds.contains(u.chatId));
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterSpaceConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
List<ConstructAnalyticsEvent> filterSpaceConstructs(
|
||||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) {
|
||||
final List<String> chatIds = space.spaceChildren
|
||||
|
|
@ -383,21 +493,20 @@ class AnalyticsController extends BaseController {
|
|||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered = filtered
|
||||
.where((e) => e.content.uses.any((u) => chatIds.contains(u.chatId)))
|
||||
.toList();
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
|
||||
for (final construct in filtered) {
|
||||
final lemmaUses = construct.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere((u) => !chatIds.contains(u.chatId));
|
||||
}
|
||||
}
|
||||
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere(
|
||||
(u) => !chatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent>? getConstructsLocal({
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required ConstructType constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
|
|
@ -419,7 +528,7 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
void cacheConstructs({
|
||||
required ConstructType constructType,
|
||||
required List<ConstructEvent> events,
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
}) {
|
||||
|
|
@ -434,14 +543,13 @@ class AnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> getMyConstructs({
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
required String langCode,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructEvent> unfilteredConstructs = await allMyConstructs(
|
||||
langCode,
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allMyConstructs(
|
||||
type: constructType,
|
||||
);
|
||||
|
||||
|
|
@ -451,39 +559,34 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
langCode: langCode,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> getSpaceConstructs({
|
||||
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
|
||||
required ConstructType constructType,
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required String langCode,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructEvent> unfilteredConstructs =
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allSpaceMemberConstructs(
|
||||
space,
|
||||
langCode,
|
||||
type: constructType,
|
||||
);
|
||||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
langCode: langCode,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> filterConstructs({
|
||||
required List<ConstructEvent> unfilteredConstructs,
|
||||
required String langCode,
|
||||
Future<List<ConstructAnalyticsEvent>> filterConstructs({
|
||||
required List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
|
|
@ -495,10 +598,12 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
for (int i = 0; i < unfilteredConstructs.length; i++) {
|
||||
final construct = unfilteredConstructs[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere(
|
||||
(u) => u.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate),
|
||||
);
|
||||
final lemmaUses = construct.content.uses;
|
||||
for (final lemmaUse in lemmaUses) {
|
||||
lemmaUse.uses.removeWhere(
|
||||
(u) => u.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
|
||||
|
|
@ -512,12 +617,7 @@ class AnalyticsController extends BaseController {
|
|||
"student filtering not available for default filter ${defaultSelected.type}",
|
||||
);
|
||||
}
|
||||
final Room? analyticsRoom =
|
||||
studentAnalyticsRoom(selected!.id, langCode);
|
||||
if (analyticsRoom == null) {
|
||||
throw Exception("analyticsRoom missing in filterConstructs");
|
||||
}
|
||||
return filterStudentConstructs(unfilteredConstructs, selected.id);
|
||||
return filterStudentConstructs(unfilteredConstructs, selected!.id);
|
||||
case AnalyticsEntryType.room:
|
||||
return filterRoomConstructs(unfilteredConstructs, selected?.id);
|
||||
case AnalyticsEntryType.privateChats:
|
||||
|
|
@ -531,14 +631,14 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>?> setConstructs({
|
||||
Future<List<ConstructAnalyticsEvent>?> setConstructs({
|
||||
required ConstructType constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = false,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
final List<ConstructEvent>? local = getConstructsLocal(
|
||||
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
|
|
@ -559,50 +659,31 @@ class AnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
|
||||
final String? roomID = space?.id ?? selected?.id;
|
||||
final String? langCode = getLangCode(
|
||||
space: space,
|
||||
roomID: roomID,
|
||||
);
|
||||
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in getConstructs",
|
||||
data: {
|
||||
"constructType": constructType,
|
||||
"AnalyticsEntryType": defaultSelected.type,
|
||||
"AnalyticsEntryId": defaultSelected.id,
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
throw "langCode missing in getConstructs";
|
||||
}
|
||||
|
||||
final filteredConstructs = space == null
|
||||
? await getMyConstructs(
|
||||
constructType: constructType,
|
||||
langCode: langCode,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
)
|
||||
: await getSpaceConstructs(
|
||||
constructType: constructType,
|
||||
space: space,
|
||||
langCode: langCode,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
|
||||
_constructs = removeIT
|
||||
? filteredConstructs
|
||||
.where(
|
||||
(element) =>
|
||||
element.content.lemma != "Try interactive translation" &&
|
||||
element.content.lemma != "itStart" &&
|
||||
element.content.lemma != MatchRuleIds.interactiveTranslation,
|
||||
)
|
||||
.toList()
|
||||
: filteredConstructs;
|
||||
if (removeIT) {
|
||||
for (final construct in filteredConstructs) {
|
||||
construct.content.uses.removeWhere(
|
||||
(element) =>
|
||||
element.lemma == "Try interactive translation" ||
|
||||
element.lemma == "itStart" ||
|
||||
element.lemma == MatchRuleIds.interactiveTranslation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_constructs = filteredConstructs;
|
||||
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
|
|
@ -621,7 +702,7 @@ class AnalyticsController extends BaseController {
|
|||
class ConstructCacheEntry {
|
||||
final TimeSpan timeSpan;
|
||||
final ConstructType type;
|
||||
final List<ConstructEvent> events;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
AnalyticsSelected? selected;
|
||||
|
||||
|
|
@ -634,20 +715,18 @@ class ConstructCacheEntry {
|
|||
});
|
||||
}
|
||||
|
||||
class CacheModel {
|
||||
class AnalyticsCacheModel {
|
||||
TimeSpan timeSpan;
|
||||
ChartAnalyticsModel chartAnalyticsModel;
|
||||
String? classId;
|
||||
String? chatId;
|
||||
String? studentId;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
AnalyticsSelected? selected;
|
||||
late DateTime _createdAt;
|
||||
|
||||
CacheModel({
|
||||
AnalyticsCacheModel({
|
||||
required this.timeSpan,
|
||||
required this.classId,
|
||||
required this.chartAnalyticsModel,
|
||||
required this.chatId,
|
||||
required this.studentId,
|
||||
required this.defaultSelected,
|
||||
this.selected,
|
||||
}) {
|
||||
_createdAt = DateTime.now();
|
||||
}
|
||||
|
|
@ -656,10 +735,3 @@ class CacheModel {
|
|||
DateTime.now().difference(_createdAt).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
|
||||
}
|
||||
|
||||
// class ListTotals {
|
||||
// String listName;
|
||||
// ConstructUses vocabUse;
|
||||
|
||||
// ListTotals({required this.listName, required this.vocabUse});
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,133 +1,312 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../extensions/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension.dart';
|
||||
import '../models/constructs_analytics_model.dart';
|
||||
import '../models/student_analytics_event.dart';
|
||||
|
||||
class MyAnalyticsController {
|
||||
class MyAnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
String? get _userId => _pangeaController.matrixState.client.userID;
|
||||
final List<String> analyticsEventTypes = [
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
PangeaEventTypes.construct,
|
||||
];
|
||||
|
||||
//PTODO - locally cache and update periodically
|
||||
Future<void> handleMessage(
|
||||
Room room,
|
||||
RecentMessageRecord messageRecord, {
|
||||
bool isEdit = false,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint("in handle message with type ${messageRecord.useType}");
|
||||
if (_userId == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "null userId in updateAnalytics",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _pangeaController.classController.addDirectChatsToClasses(room);
|
||||
//expanding this to all parents of the room
|
||||
// final List<Room> spaces = room.immediateClassParents;
|
||||
final List<Room> spaces = room.pangeaSpaceParents;
|
||||
// janky but probably stays until we have a class analytics bot added by
|
||||
// default to all chats
|
||||
|
||||
final List<StudentAnalyticsEvent?> events = await analyticsEvents(spaces);
|
||||
|
||||
for (final event in events) {
|
||||
if (event != null) {
|
||||
event.handleNewMessage(messageRecord, isEdit: isEdit);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> analyticsEvents(
|
||||
List<Room> spaces,
|
||||
Future<void> sendAllAnalyticsEvents(
|
||||
Room analyticsRoom,
|
||||
) async {
|
||||
final List<Future<StudentAnalyticsEvent?>> events = [];
|
||||
if (_userId != null) {
|
||||
for (final space in spaces) {
|
||||
events.add(space.getStudentAnalytics(_userId!));
|
||||
}
|
||||
final String? langCode = analyticsRoom.madeForLang;
|
||||
if (langCode == null) {
|
||||
debugPrint("no lang code found for analytics room: ${analyticsRoom.id}");
|
||||
return;
|
||||
}
|
||||
return Future.wait(events);
|
||||
}
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> allMyAnalyticsEvents() async =>
|
||||
analyticsEvents(
|
||||
await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn,
|
||||
final Map<String, AnalyticsEvent?> prevEvents = {};
|
||||
for (final type in analyticsEventTypes) {
|
||||
final prevEvent = await analyticsRoom.getLastAnalyticsEvent(type);
|
||||
prevEvents[type] = prevEvent;
|
||||
}
|
||||
|
||||
final List<DateTime?> lastUpdates = prevEvents.values
|
||||
.map((e) => e?.content.lastUpdated)
|
||||
.cast<DateTime?>()
|
||||
.toList();
|
||||
|
||||
DateTime? earliestLastUpdated;
|
||||
if (!lastUpdates.any((updated) => updated == null)) {
|
||||
earliestLastUpdated = lastUpdates.reduce(
|
||||
(min, e) => e!.isBefore(min!) ? e : min,
|
||||
);
|
||||
}
|
||||
|
||||
final List<RecentMessageRecord> analyticsContent = [];
|
||||
final List<OneConstructUse> constructsContent = [];
|
||||
|
||||
for (final Room chat in _studentChats) {
|
||||
final String? chatLangCode =
|
||||
_pangeaController.languageController.activeL2Code(roomID: chat.id);
|
||||
if (chatLangCode != langCode) continue;
|
||||
|
||||
final List<PangeaMessageEvent> recentMsgs =
|
||||
await chat.myMessageEventsInChat(
|
||||
since: earliestLastUpdated,
|
||||
);
|
||||
|
||||
Future<void> saveConstructsMixed(
|
||||
List<OneConstructUse> allUses,
|
||||
String langCode, {
|
||||
bool isEdit = false,
|
||||
}) async {
|
||||
try {
|
||||
final Map<String, List<OneConstructUse>> aggregatedVocabUse = {};
|
||||
for (final use in allUses) {
|
||||
if (use.lemma == null) continue;
|
||||
aggregatedVocabUse[use.lemma!] ??= [];
|
||||
aggregatedVocabUse[use.lemma]!.add(use);
|
||||
}
|
||||
final Room analyticsRoom = await _pangeaController.matrixState.client
|
||||
.getMyAnalyticsRoom(langCode);
|
||||
analyticsContent.addAll(
|
||||
formatAnalyticsContent(
|
||||
recentMsgs,
|
||||
prevEvents[PangeaEventTypes.summaryAnalytics]
|
||||
as SummaryAnalyticsEvent?,
|
||||
),
|
||||
);
|
||||
|
||||
final List<Future<void>> saveFutures = [];
|
||||
for (final uses in aggregatedVocabUse.entries) {
|
||||
debugPrint("saving of type ${uses.value.first.constructType}");
|
||||
saveFutures.add(
|
||||
analyticsRoom.saveConstructUsesSameLemma(
|
||||
uses.key,
|
||||
uses.value.first.constructType ?? ConstructType.grammar,
|
||||
uses.value,
|
||||
isEdit: isEdit,
|
||||
constructsContent.addAll(
|
||||
formatConstructsContent(
|
||||
recentMsgs,
|
||||
prevEvents[PangeaEventTypes.construct] as ConstructAnalyticsEvent?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (analyticsContent.isNotEmpty) {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: analyticsContent,
|
||||
lastUpdated: DateTime.now(),
|
||||
prevEventId:
|
||||
prevEvents[PangeaEventTypes.summaryAnalytics]?.event.eventId,
|
||||
prevLastUpdated:
|
||||
prevEvents[PangeaEventTypes.summaryAnalytics]?.content.lastUpdated,
|
||||
);
|
||||
await analyticsRoom.sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
}
|
||||
|
||||
if (constructsContent.isNotEmpty) {
|
||||
final Map<String, List<OneConstructUse>> lemmasUses = {};
|
||||
for (final use in constructsContent) {
|
||||
if (use.lemma == null) {
|
||||
debugPrint("use has no lemma!");
|
||||
continue;
|
||||
}
|
||||
lemmasUses[use.lemma!] ??= [];
|
||||
lemmasUses[use.lemma]!.add(use);
|
||||
}
|
||||
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
type: ConstructType.grammar,
|
||||
uses: lemmasUses.entries
|
||||
.map(
|
||||
(entry) => LemmaConstructsModel(
|
||||
lemma: entry.key,
|
||||
uses: entry.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
lastUpdated: DateTime.now(),
|
||||
prevEventId: prevEvents[PangeaEventTypes.construct]?.event.eventId,
|
||||
prevLastUpdated:
|
||||
prevEvents[PangeaEventTypes.construct]?.content.lastUpdated,
|
||||
);
|
||||
|
||||
await analyticsRoom.sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> formatAnalyticsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
SummaryAnalyticsEvent? prevEvent,
|
||||
) {
|
||||
List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
if (prevEvent?.content.lastUpdated != null) {
|
||||
filtered = recentMsgs
|
||||
.where(
|
||||
(msg) => msg.event.originServerTs.isAfter(
|
||||
prevEvent!.content.lastUpdated!,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final List<String> addedMsgIds =
|
||||
prevEvent?.content.messages.map((msg) => msg.eventId).toList() ?? [];
|
||||
|
||||
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
|
||||
|
||||
final List<RecentMessageRecord> records = filtered
|
||||
.map(
|
||||
(msg) => RecentMessageRecord(
|
||||
eventId: msg.eventId,
|
||||
chatId: msg.room.id,
|
||||
useType: msg.useType,
|
||||
time: msg.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
.toList();
|
||||
|
||||
await Future.wait(saveFutures);
|
||||
} catch (err, s) {
|
||||
return records;
|
||||
}
|
||||
|
||||
List<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
ConstructAnalyticsEvent? prevEvent,
|
||||
) {
|
||||
List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
if (prevEvent?.content.lastUpdated != null) {
|
||||
filtered = recentMsgs
|
||||
.where(
|
||||
(msg) => msg.event.originServerTs.isAfter(
|
||||
prevEvent!.content.lastUpdated!,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final List<String> addedMsgIds = prevEvent?.content.uses
|
||||
.map((lemmause) => lemmause.uses.map((use) => use.msgId))
|
||||
.expand((element) => element)
|
||||
.where((element) => element != null)
|
||||
.cast<String>()
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
|
||||
|
||||
final List<OneConstructUse> uses = filtered
|
||||
.map(
|
||||
(msg) => msg.originalSent?.choreo?.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<List<OneConstructUse>>()
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<Room> _studentChats = [];
|
||||
|
||||
Future<void> setStudentChats() async {
|
||||
final List<String> teacherRoomIds =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
_studentChats = _pangeaController.matrixState.client.rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!teacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
setState(data: _studentChats);
|
||||
}
|
||||
|
||||
List<Room> get studentChats {
|
||||
try {
|
||||
if (_studentChats.isNotEmpty) return _studentChats;
|
||||
setStudentChats();
|
||||
return _studentChats;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
if (!kDebugMode) rethrow;
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
List<Room> _studentSpaces = [];
|
||||
|
||||
Future<void> setStudentSpaces() async {
|
||||
if (_studentSpaces.isNotEmpty) return;
|
||||
_studentSpaces = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
}
|
||||
|
||||
List<Room> get studentSpaces {
|
||||
try {
|
||||
if (_studentSpaces.isNotEmpty) return _studentSpaces;
|
||||
setStudentSpaces();
|
||||
return _studentSpaces;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// on the off chance that the user is in a class but doesn't have an analytics
|
||||
// room for the target language of that class, create the analytics room(s)
|
||||
Future<List<Room>> createMissingAnalyticsRoom() async {
|
||||
List<String> targetLangs = [];
|
||||
final String? userL2 = _pangeaController.languageController.activeL2Code();
|
||||
if (userL2 != null) targetLangs.add(userL2);
|
||||
final List<String?> spaceL2s = studentSpaces
|
||||
.map(
|
||||
(space) => _pangeaController.languageController.activeL2Code(
|
||||
roomID: space.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast<String>());
|
||||
targetLangs = targetLangs.toSet().toList();
|
||||
for (final String langCode in targetLangs) {
|
||||
await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
}
|
||||
return _pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
}
|
||||
|
||||
Future<void> updateAnalytics() async {
|
||||
await setStudentChats();
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
analyticsRooms.addAll(await createMissingAnalyticsRoom());
|
||||
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
await sendAllAnalyticsEvents(analyticsRoom);
|
||||
}
|
||||
}
|
||||
|
||||
// used to aggregate ConstructEvents, from multiple senders (students) with the same lemma
|
||||
List<AggregateConstructUses> aggregateConstructData(
|
||||
List<ConstructEvent> constructs,
|
||||
List<ConstructAnalyticsEvent> constructs,
|
||||
) {
|
||||
final Map<String, List<ConstructEvent>> lemmasToConstructs = {};
|
||||
final Map<String, List<LemmaConstructsModel>> lemmasToConstructs = {};
|
||||
for (final construct in constructs) {
|
||||
lemmasToConstructs[construct.content.lemma] ??= [];
|
||||
lemmasToConstructs[construct.content.lemma]!.add(construct);
|
||||
for (final lemmaUses in construct.content.uses) {
|
||||
lemmasToConstructs[lemmaUses.lemma] ??= [];
|
||||
lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses);
|
||||
}
|
||||
}
|
||||
|
||||
final List<AggregateConstructUses> aggregatedConstructs = [];
|
||||
for (final lemmaToConstructs in lemmasToConstructs.entries) {
|
||||
final List<ConstructEvent> lemmaConstructs = lemmaToConstructs.value;
|
||||
final List<LemmaConstructsModel> lemmaConstructs =
|
||||
lemmaToConstructs.value;
|
||||
final AggregateConstructUses aggregatedData = AggregateConstructUses(
|
||||
constructs: lemmaConstructs,
|
||||
lemmaUses: lemmaConstructs,
|
||||
);
|
||||
aggregatedConstructs.add(aggregatedData);
|
||||
}
|
||||
|
|
@ -136,24 +315,23 @@ class MyAnalyticsController {
|
|||
}
|
||||
|
||||
class AggregateConstructUses {
|
||||
final List<ConstructEvent> _constructs;
|
||||
final List<LemmaConstructsModel> _lemmaUses;
|
||||
|
||||
AggregateConstructUses({required List<ConstructEvent> constructs})
|
||||
: _constructs = constructs;
|
||||
AggregateConstructUses({required List<LemmaConstructsModel> lemmaUses})
|
||||
: _lemmaUses = lemmaUses;
|
||||
|
||||
String get lemma {
|
||||
assert(
|
||||
_constructs.isNotEmpty &&
|
||||
_constructs.every(
|
||||
(construct) =>
|
||||
construct.content.lemma == _constructs.first.content.lemma,
|
||||
_lemmaUses.isNotEmpty &&
|
||||
_lemmaUses.every(
|
||||
(construct) => construct.lemma == _lemmaUses.first.lemma,
|
||||
),
|
||||
);
|
||||
return _constructs.first.content.lemma;
|
||||
return _lemmaUses.first.lemma;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get uses => _constructs
|
||||
.map((construct) => construct.content.uses)
|
||||
List<OneConstructUse> get uses => _lemmaUses
|
||||
.map((lemmaUse) => lemmaUse.uses)
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
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';
|
||||
|
|
@ -11,8 +9,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../utils/p_store.dart';
|
||||
|
||||
extension PangeaClient on Client {
|
||||
List<Room> get classes => rooms.where((e) => e.isPangeaClass).toList();
|
||||
|
||||
|
|
@ -97,23 +93,6 @@ extension PangeaClient on Client {
|
|||
return teachers;
|
||||
}
|
||||
|
||||
Future<void> updateMyLearningAnalyticsForAllClassesImIn([
|
||||
PLocalStore? storageService,
|
||||
]) async {
|
||||
try {
|
||||
final List<Future<void>> updateFutures = [];
|
||||
for (final classRoom in classesAndExchangesImIn) {
|
||||
updateFutures
|
||||
.add(classRoom.updateMyLearningAnalyticsForClass(storageService));
|
||||
}
|
||||
await Future.wait(updateFutures);
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
// debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
// get analytics room matching targetlanguage
|
||||
// if not present, create it and invite teachers of that language
|
||||
// set description to let people know what the hell it is
|
||||
|
|
@ -143,7 +122,7 @@ extension PangeaClient on Client {
|
|||
});
|
||||
if (analyticsRoom != null &&
|
||||
analyticsRoom.membership == Membership.invite) {
|
||||
debugger(when: kDebugMode);
|
||||
// debugger(when: kDebugMode);
|
||||
analyticsRoom
|
||||
.join()
|
||||
.onError(
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ 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';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -22,15 +25,9 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
|
||||
import '../../config/app_config.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../matrix_event_wrappers/construct_analytics_event.dart';
|
||||
import '../models/choreo_record.dart';
|
||||
import '../models/constructs_analytics_model.dart';
|
||||
import '../models/representation_content_model.dart';
|
||||
import '../models/student_analytics_event.dart';
|
||||
import '../models/student_analytics_summary_model.dart';
|
||||
import '../utils/p_store.dart';
|
||||
import 'client_extension.dart';
|
||||
|
||||
extension PangeaRoom on Room {
|
||||
|
|
@ -303,6 +300,12 @@ extension PangeaRoom on Room {
|
|||
bool isMadeByUser(String userId) =>
|
||||
getState(EventTypes.RoomCreate)?.senderId == userId;
|
||||
|
||||
String? get madeForLang {
|
||||
final creationContent = getState(EventTypes.RoomCreate)?.content;
|
||||
return creationContent?.tryGet<String>(ModelKey.langCode) ??
|
||||
creationContent?.tryGet<String>(ModelKey.oldLangCode);
|
||||
}
|
||||
|
||||
bool isMadeForLang(String langCode) {
|
||||
final creationContent = getState(EventTypes.RoomCreate)?.content;
|
||||
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
|
||||
|
|
@ -328,55 +331,81 @@ extension PangeaRoom on Room {
|
|||
return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", "");
|
||||
}
|
||||
|
||||
StudentAnalyticsEvent? _getStudentAnalyticsLocal(String studentId) {
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "calling getStudentAnalyticsLocal on non-space room",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// StudentAnalyticsEvent? _getStudentAnalyticsLocal() {
|
||||
// if (!isAnalyticsRoom) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(
|
||||
// m: "calling getStudentAnalyticsLocal on non-analytics room",
|
||||
// s: StackTrace.current,
|
||||
// );
|
||||
// return null;
|
||||
// }
|
||||
|
||||
final Event? matrixEvent = getState(
|
||||
PangeaEventTypes.studentAnalyticsSummary,
|
||||
studentId,
|
||||
);
|
||||
// final Event? matrixEvent = getState(PangeaEventTypes.summaryAnalytics);
|
||||
|
||||
return matrixEvent != null
|
||||
? StudentAnalyticsEvent(event: matrixEvent)
|
||||
: null;
|
||||
}
|
||||
// return matrixEvent != null
|
||||
// ? StudentAnalyticsEvent(event: matrixEvent)
|
||||
// : null;
|
||||
// }
|
||||
|
||||
Future<StudentAnalyticsEvent?> getStudentAnalytics(
|
||||
String studentId, {
|
||||
bool forcedUpdate = false,
|
||||
}) async {
|
||||
try {
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception("calling getStudentAnalyticsLocal on non-space room");
|
||||
}
|
||||
StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(studentId);
|
||||
// Future<Map<String, DateTime>> lemmasLastUpdated() async {
|
||||
// try {
|
||||
// if (!isAnalyticsRoom) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "calling lemmasLastUpdated on non-analytics room",
|
||||
// );
|
||||
// }
|
||||
|
||||
if (localEvent == null) {
|
||||
await postLoad();
|
||||
localEvent = _getStudentAnalyticsLocal(studentId);
|
||||
}
|
||||
// await postLoad();
|
||||
// final entries = states[PangeaEventTypes.vocab]?.entries.toList();
|
||||
// if (entries != null && entries.isNotEmpty) {
|
||||
// final Map<String, DateTime> resultMap = {};
|
||||
// for (final entry in entries) {
|
||||
// // migration - don't count uses without unique IDs
|
||||
// if (ConstructEvent(event: entry.value)
|
||||
// .content
|
||||
// .uses
|
||||
// .any((use) => use.id != null)) {
|
||||
// resultMap[entry.key] = entry.value.originServerTs;
|
||||
// }
|
||||
// }
|
||||
// return resultMap;
|
||||
// }
|
||||
// return {};
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (studentId == client.userID && localEvent == null) {
|
||||
final Event? matrixEvent = await _createStudentAnalyticsEvent();
|
||||
if (matrixEvent != null) {
|
||||
localEvent = StudentAnalyticsEvent(event: matrixEvent);
|
||||
}
|
||||
}
|
||||
// Future<StudentAnalyticsEvent?> getStudentAnalyticsEvent({
|
||||
// bool forcedUpdate = false,
|
||||
// }) async {
|
||||
// try {
|
||||
// if (!isAnalyticsRoom) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "calling getStudentAnalyticsLocal on non-analytics room",
|
||||
// );
|
||||
// }
|
||||
|
||||
return localEvent;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
// await postLoad();
|
||||
// StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal();
|
||||
|
||||
// if (isRoomOwner && localEvent == null) {
|
||||
// final Event? matrixEvent = await _createStudentAnalyticsEvent();
|
||||
// if (matrixEvent != null) {
|
||||
// localEvent = StudentAnalyticsEvent(event: matrixEvent);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return localEvent;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
void checkClass() {
|
||||
if (!isSpace) {
|
||||
|
|
@ -414,201 +443,109 @@ extension PangeaRoom on Room {
|
|||
: participants;
|
||||
}
|
||||
|
||||
/// if [studentIds] is null, returns all students
|
||||
Future<List<StudentAnalyticsEvent?>> getClassAnalytics([
|
||||
List<String>? studentIds,
|
||||
]) async {
|
||||
await postLoad();
|
||||
await requestParticipants();
|
||||
final List<Future<StudentAnalyticsEvent?>> sassFutures = [];
|
||||
final List<String> filteredIds = students
|
||||
.where(
|
||||
(element) => studentIds == null || studentIds.contains(element.id),
|
||||
)
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
for (final id in filteredIds) {
|
||||
sassFutures.add(
|
||||
getStudentAnalytics(
|
||||
id,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Future.wait(sassFutures);
|
||||
}
|
||||
|
||||
/// if [isSpace]
|
||||
/// for all child chats, call _getChatAnalyticsGlobal and merge results
|
||||
/// else
|
||||
/// get analytics from pangea chat server
|
||||
/// do any needed conversion work
|
||||
/// 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",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (client.userID == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception("null userId in createStudentAnalytics");
|
||||
}
|
||||
// Future<Event?> _createStudentAnalyticsEvent() async {
|
||||
// try {
|
||||
// if (!isAnalyticsRoom) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "calling _createStudentAnalyticsEvent on non-analytics room",
|
||||
// );
|
||||
// }
|
||||
|
||||
final String eventId = await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.studentAnalyticsSummary,
|
||||
client.userID!,
|
||||
StudentAnalyticsSummary(
|
||||
// studentId: client.userID!,
|
||||
lastUpdated: DateTime.now(),
|
||||
messages: [],
|
||||
).toJson(),
|
||||
);
|
||||
final Event? event = await getEventById(eventId);
|
||||
// await postLoad();
|
||||
// if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) {
|
||||
// ErrorHandler.logError(
|
||||
// m: "null powerLevels in createStudentAnalytics",
|
||||
// s: StackTrace.current,
|
||||
// );
|
||||
// return null;
|
||||
// }
|
||||
// if (client.userID == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception("null userId in createStudentAnalytics");
|
||||
// }
|
||||
|
||||
if (event == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception(
|
||||
"null event after creation with eventId $eventId in createStudentAnalytics",
|
||||
);
|
||||
}
|
||||
return event;
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// final String eventId = await client.setRoomStateWithKey(
|
||||
// id,
|
||||
// PangeaEventTypes.studentAnalyticsSummary,
|
||||
// '',
|
||||
// StudentAnalyticsSummary(
|
||||
// lastUpdated: null,
|
||||
// messages: [],
|
||||
// ).toJson(),
|
||||
// );
|
||||
// final Event? event = await getEventById(eventId);
|
||||
|
||||
/// for each chat in class
|
||||
/// get timeline back to january 15
|
||||
/// get messages
|
||||
/// discard timeline
|
||||
/// save messages to StudentAnalyticsSummary
|
||||
Future<void> updateMyLearningAnalyticsForClass([
|
||||
PLocalStore? storageService,
|
||||
]) async {
|
||||
try {
|
||||
final String migratedAnalyticsKey =
|
||||
"MIGRATED_ANALYTICS_KEY${id.localpart}";
|
||||
// if (event == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "null event after creation with eventId $eventId in createStudentAnalytics",
|
||||
// );
|
||||
// }
|
||||
// return event;
|
||||
// } catch (err, stack) {
|
||||
// ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (storageService?.read(
|
||||
migratedAnalyticsKey,
|
||||
local: true,
|
||||
) ??
|
||||
false) return;
|
||||
|
||||
if (!isPangeaClass && !isExchange) {
|
||||
throw Exception(
|
||||
"In updateMyLearningAnalyticsForClass with room that is not not a class",
|
||||
);
|
||||
}
|
||||
|
||||
if (client.userID == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
final StudentAnalyticsEvent? myAnalEvent =
|
||||
await getStudentAnalytics(client.userID!);
|
||||
|
||||
if (myAnalEvent == null) {
|
||||
debugPrint("null analytcs event for $id");
|
||||
if (pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) {
|
||||
// debugger(when: kDebugMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final updateMessages = await _messageListForAllChildChats;
|
||||
updateMessages.removeWhere(
|
||||
(element) => myAnalEvent.content.messages.any(
|
||||
(e) => e.eventId == element.eventId,
|
||||
),
|
||||
);
|
||||
myAnalEvent.bulkUpdate(updateMessages);
|
||||
|
||||
await storageService?.save(
|
||||
migratedAnalyticsKey,
|
||||
true,
|
||||
local: true,
|
||||
);
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
// debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RecentMessageRecord>> get _messageListForAllChildChats async {
|
||||
try {
|
||||
if (!isSpace) return [];
|
||||
final List<Room> spaceChats = spaceChildren
|
||||
.where((e) => e.roomId != null)
|
||||
.map((e) => client.getRoomById(e.roomId!))
|
||||
.where((element) => element != null)
|
||||
.cast<Room>()
|
||||
.where((element) => !element.isSpace)
|
||||
.toList();
|
||||
|
||||
final List<Future<List<RecentMessageRecord>>> msgListFutures = [];
|
||||
for (final chat in spaceChats) {
|
||||
msgListFutures.add(chat._messageListForChat);
|
||||
}
|
||||
final List<List<RecentMessageRecord>> msgLists =
|
||||
await Future.wait(msgListFutures);
|
||||
|
||||
final List<RecentMessageRecord> joined = [];
|
||||
for (final msgList in msgLists) {
|
||||
joined.addAll(msgList);
|
||||
}
|
||||
return joined;
|
||||
} catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RecentMessageRecord>> get _messageListForChat async {
|
||||
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
|
||||
DateTime? since,
|
||||
}) async {
|
||||
try {
|
||||
int numberOfSearches = 0;
|
||||
|
||||
if (isSpace) {
|
||||
throw Exception(
|
||||
"In messageListForChat with room that is not a chat",
|
||||
);
|
||||
}
|
||||
final Timeline timeline = await getTimeline();
|
||||
|
||||
while (timeline.canRequestHistory && numberOfSearches < 50) {
|
||||
await timeline.requestHistory(historyCount: 100);
|
||||
try {
|
||||
await timeline.requestHistory();
|
||||
} catch (err) {
|
||||
break;
|
||||
}
|
||||
numberOfSearches += 1;
|
||||
}
|
||||
if (timeline.canRequestHistory) {
|
||||
debugger(when: kDebugMode);
|
||||
if (timeline.events.any(
|
||||
(event) => event.originServerTs.isAfter(since ?? DateTime.now()),
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final List<RecentMessageRecord> msgs = [];
|
||||
for (final event in timeline.events) {
|
||||
if (event.senderId == client.userID &&
|
||||
event.type == EventTypes.Message &&
|
||||
event.content['msgtype'] == MessageTypes.Text) {
|
||||
final List<PangeaMessageEvent> msgs = [];
|
||||
for (Event event in timeline.events) {
|
||||
final bool hasAnalytics = (event.senderId == client.userID) &&
|
||||
(event.type == EventTypes.Message) &&
|
||||
(event.content['msgtype'] == MessageTypes.Text &&
|
||||
!(event.relationshipType == RelationshipTypes.edit));
|
||||
if (hasAnalytics &&
|
||||
(since == null || event.originServerTs.isAfter(since))) {
|
||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) {
|
||||
event = event
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.originServerTs.compareTo(a.originServerTs),
|
||||
)
|
||||
.firstOrNull ??
|
||||
event;
|
||||
}
|
||||
final PangeaMessageEvent pMsgEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: true,
|
||||
);
|
||||
msgs.add(
|
||||
RecentMessageRecord(
|
||||
eventId: event.eventId,
|
||||
chatId: id,
|
||||
useType: pMsgEvent.useType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
msgs.add(pMsgEvent);
|
||||
}
|
||||
}
|
||||
return msgs;
|
||||
|
|
@ -673,139 +610,152 @@ extension PangeaRoom on Room {
|
|||
}
|
||||
}
|
||||
|
||||
ConstructEvent? _vocabEventLocal(String lemma) {
|
||||
if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
// ConstructEvent? vocabEventLocal(String lemma) {
|
||||
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
|
||||
final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma);
|
||||
// final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma);
|
||||
|
||||
return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null;
|
||||
}
|
||||
// return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null;
|
||||
// }
|
||||
|
||||
bool get isRoomOwner =>
|
||||
getState(EventTypes.RoomCreate)?.senderId == client.userID;
|
||||
|
||||
Future<ConstructEvent> vocabEvent(
|
||||
String lemma,
|
||||
ConstructType type, [
|
||||
bool makeIfNull = false,
|
||||
]) async {
|
||||
try {
|
||||
if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
// Future<ConstructEvent> vocabEvent(
|
||||
// String lemma,
|
||||
// ConstructType type, [
|
||||
// bool makeIfNull = false,
|
||||
// ]) async {
|
||||
// try {
|
||||
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
|
||||
ConstructEvent? localEvent = _vocabEventLocal(lemma);
|
||||
// ConstructEvent? localEvent = vocabEventLocal(lemma);
|
||||
|
||||
if (localEvent != null) return localEvent;
|
||||
// if (localEvent != null) return localEvent;
|
||||
|
||||
await postLoad();
|
||||
localEvent = _vocabEventLocal(lemma);
|
||||
// await postLoad();
|
||||
// localEvent = vocabEventLocal(lemma);
|
||||
|
||||
if (localEvent == null && isRoomOwner && makeIfNull) {
|
||||
final Event matrixEvent = await _createVocabEvent(lemma, type);
|
||||
localEvent = ConstructEvent(event: matrixEvent);
|
||||
}
|
||||
// if (localEvent == null && isRoomOwner && makeIfNull) {
|
||||
// final Event matrixEvent = await _createVocabEvent(lemma, type);
|
||||
// localEvent = ConstructEvent(event: matrixEvent);
|
||||
// }
|
||||
|
||||
return localEvent!;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
// return localEvent!;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<List<OneConstructUse>> removeEdittedLemmas(
|
||||
List<OneConstructUse> lemmaUses,
|
||||
) async {
|
||||
final List<String> removeUses = [];
|
||||
for (final use in lemmaUses) {
|
||||
if (use.msgId == null) continue;
|
||||
final List<String> removeIds = await client.getEditHistory(
|
||||
use.chatId,
|
||||
use.msgId!,
|
||||
);
|
||||
removeUses.addAll(removeIds);
|
||||
}
|
||||
lemmaUses.removeWhere((use) => removeUses.contains(use.msgId));
|
||||
final allEvents = await allConstructEvents;
|
||||
for (final constructEvent in allEvents) {
|
||||
await constructEvent.removeEdittedUses(removeUses, client);
|
||||
}
|
||||
return lemmaUses;
|
||||
}
|
||||
// Future<List<OneConstructUse>> removeEdittedLemmas(
|
||||
// List<OneConstructUse> lemmaUses,
|
||||
// ) async {
|
||||
// final List<String> removeUses = [];
|
||||
// for (final use in lemmaUses) {
|
||||
// if (use.msgId == null) continue;
|
||||
// final List<String> removeIds = await client.getEditHistory(
|
||||
// use.chatId,
|
||||
// use.msgId!,
|
||||
// );
|
||||
// removeUses.addAll(removeIds);
|
||||
// }
|
||||
// lemmaUses.removeWhere((use) => removeUses.contains(use.msgId));
|
||||
// final allEvents = await allConstructEvents;
|
||||
// for (final constructEvent in allEvents) {
|
||||
// await constructEvent.removeEdittedUses(removeUses, client);
|
||||
// }
|
||||
// return lemmaUses;
|
||||
// }
|
||||
|
||||
Future<void> saveConstructUsesSameLemma(
|
||||
String lemma,
|
||||
ConstructType type,
|
||||
List<OneConstructUse> lemmaUses, {
|
||||
bool isEdit = false,
|
||||
}) async {
|
||||
final ConstructEvent? localEvent = _vocabEventLocal(lemma);
|
||||
// Future<void> saveConstructUsesSameLemma(
|
||||
// String lemma,
|
||||
// ConstructType type,
|
||||
// List<OneConstructUse> lemmaUses, {
|
||||
// bool isEdit = false,
|
||||
// }) async {
|
||||
// final ConstructEvent? localEvent = vocabEventLocal(lemma);
|
||||
|
||||
if (isEdit) {
|
||||
lemmaUses = await removeEdittedLemmas(lemmaUses);
|
||||
}
|
||||
// if (isEdit) {
|
||||
// lemmaUses = await removeEdittedLemmas(lemmaUses);
|
||||
// }
|
||||
|
||||
if (localEvent == null) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.vocab,
|
||||
lemma,
|
||||
ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(),
|
||||
);
|
||||
} else {
|
||||
localEvent.addAll(lemmaUses);
|
||||
await updateStateEvent(localEvent.event);
|
||||
}
|
||||
}
|
||||
// // final waitForUpdate = client.onRoomState.stream.firstWhere(
|
||||
// // (Event event) =>
|
||||
// // event.type == PangeaEventTypes.vocab && event.stateKey == lemma,
|
||||
// // );
|
||||
|
||||
Future<List<ConstructEvent>> get allConstructEvents async {
|
||||
await postLoad();
|
||||
return states[PangeaEventTypes.vocab]
|
||||
?.values
|
||||
.map((Event event) => ConstructEvent(event: event))
|
||||
.toList()
|
||||
.cast<ConstructEvent>() ??
|
||||
[];
|
||||
}
|
||||
// if (localEvent == null) {
|
||||
// await client.setRoomStateWithKey(
|
||||
// id,
|
||||
// PangeaEventTypes.vocab,
|
||||
// lemma,
|
||||
// ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(),
|
||||
// );
|
||||
// await postLoad();
|
||||
// } else {
|
||||
// // migration - remove uses without unique IDs,
|
||||
// // this is used to prevent duplicate saves
|
||||
// localEvent.content.uses.removeWhere((use) => use.id == null);
|
||||
// localEvent.addAll(lemmaUses);
|
||||
// await updateStateEvent(localEvent.event);
|
||||
// }
|
||||
|
||||
Future<Event> _createVocabEvent(String lemma, ConstructType type) async {
|
||||
try {
|
||||
if (!isRoomOwner) {
|
||||
throw Exception(
|
||||
"Tried to create vocab event in room where user is not owner",
|
||||
);
|
||||
}
|
||||
final String eventId = await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.vocab,
|
||||
lemma,
|
||||
ConstructUses(lemma: lemma, type: type).toJson(),
|
||||
);
|
||||
final Event? event = await getEventById(eventId);
|
||||
// // await waitForUpdate;
|
||||
// }
|
||||
|
||||
if (event == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception(
|
||||
"null event after creation with eventId $eventId in _createVocabEvent",
|
||||
);
|
||||
}
|
||||
return event;
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
// Future<List<ConstructEvent>> get allConstructEvents async {
|
||||
// await postLoad();
|
||||
// return states[PangeaEventTypes.vocab]
|
||||
// ?.values
|
||||
// .map((Event event) => ConstructEvent(event: event))
|
||||
// .toList()
|
||||
// .cast<ConstructEvent>() ??
|
||||
// [];
|
||||
// }
|
||||
|
||||
// Future<Event> _createVocabEvent(String lemma, ConstructType type) async {
|
||||
// try {
|
||||
// if (!isRoomOwner) {
|
||||
// throw Exception(
|
||||
// "Tried to create vocab event in room where user is not owner",
|
||||
// );
|
||||
// }
|
||||
// final String eventId = await client.setRoomStateWithKey(
|
||||
// id,
|
||||
// PangeaEventTypes.vocab,
|
||||
// lemma,
|
||||
// ConstructUses(lemma: lemma, type: type).toJson(),
|
||||
// );
|
||||
// final Event? event = await getEventById(eventId);
|
||||
|
||||
// if (event == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// throw Exception(
|
||||
// "null event after creation with eventId $eventId in _createVocabEvent",
|
||||
// );
|
||||
// }
|
||||
// return event;
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
/// update state event and return eventId
|
||||
Future<String> updateStateEvent(Event stateEvent) {
|
||||
Future<String> updateStateEvent(Event stateEvent) async {
|
||||
if (stateEvent.stateKey == null) {
|
||||
throw Exception("stateEvent.stateKey is null");
|
||||
}
|
||||
return client.setRoomStateWithKey(
|
||||
final String resp = await client.setRoomStateWithKey(
|
||||
id,
|
||||
stateEvent.type,
|
||||
stateEvent.stateKey!,
|
||||
stateEvent.content,
|
||||
);
|
||||
await postLoad();
|
||||
return resp;
|
||||
}
|
||||
|
||||
bool canIAddSpaceChild(Room? room) {
|
||||
|
|
@ -872,15 +822,9 @@ extension PangeaRoom on Room {
|
|||
final Map<String, dynamic>? currentPowerContent =
|
||||
currentPower?.content["events"] as Map<String, dynamic>?;
|
||||
final spaceChildPower = currentPowerContent?[EventTypes.spaceChild];
|
||||
final studentAnalyticsPower =
|
||||
currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary];
|
||||
|
||||
if ((spaceChildPower == null || studentAnalyticsPower == null) &&
|
||||
currentPowerContent != null) {
|
||||
if (spaceChildPower == null && currentPowerContent != null) {
|
||||
currentPowerContent["events"][EventTypes.spaceChild] = 0;
|
||||
currentPowerContent["events"]
|
||||
[PangeaEventTypes.studentAnalyticsSummary] = 0;
|
||||
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
EventTypes.RoomPowerLevels,
|
||||
|
|
@ -1260,4 +1204,83 @@ extension PangeaRoom on Room {
|
|||
if (firstLanguageSettings?.targetLanguage == null) return;
|
||||
await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage);
|
||||
}
|
||||
|
||||
Future<AnalyticsEvent?> getLastAnalyticsEvent(
|
||||
String type,
|
||||
) async {
|
||||
final Timeline timeline = await getTimeline();
|
||||
int requests = 0;
|
||||
Event? lastEvent = timeline.events.firstWhereOrNull(
|
||||
(event) => event.type == type,
|
||||
);
|
||||
|
||||
while (requests < 10 && timeline.canRequestHistory && lastEvent == null) {
|
||||
await timeline.requestHistory();
|
||||
lastEvent = timeline.events.firstWhereOrNull(
|
||||
(event) => event.type == type,
|
||||
);
|
||||
requests++;
|
||||
}
|
||||
|
||||
if (lastEvent == null) return null;
|
||||
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsEvent(event: lastEvent);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsEvent(event: lastEvent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<AnalyticsEvent?> getPrevAnalyticsEvent(
|
||||
AnalyticsEvent analyticsEvent,
|
||||
) async {
|
||||
if (analyticsEvent.content.prevEventId == null) {
|
||||
return null;
|
||||
}
|
||||
final Event? prevEvent = await getEventById(
|
||||
analyticsEvent.content.prevEventId!,
|
||||
);
|
||||
if (prevEvent == null) return null;
|
||||
|
||||
switch (analyticsEvent.event.type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsEvent(event: prevEvent);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsEvent(event: prevEvent);
|
||||
}
|
||||
|
||||
return null;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> getAnalyticsEvents({
|
||||
required String type,
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final AnalyticsEvent? mostRecentEvent = await getLastAnalyticsEvent(type);
|
||||
if (mostRecentEvent == null) return null;
|
||||
final List<AnalyticsEvent> events = [mostRecentEvent];
|
||||
|
||||
bool getAllEvents() =>
|
||||
since == null && events.last.content.prevEventId == null;
|
||||
|
||||
bool reachedUpdated() =>
|
||||
since != null &&
|
||||
(events.last.content.lastUpdated?.isBefore(since) ?? true);
|
||||
|
||||
while (getAllEvents() || !reachedUpdated()) {
|
||||
final AnalyticsEvent? prevEvent = await getPrevAnalyticsEvent(
|
||||
events.last,
|
||||
);
|
||||
if (prevEvent == null) break;
|
||||
events.add(prevEvent);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,60 @@
|
|||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
// import '../constants/pangea_event_types.dart';
|
||||
|
||||
class ConstructEvent {
|
||||
late Event _event;
|
||||
ConstructUses? _contentCache;
|
||||
// class ConstructEvent {
|
||||
// late Event _event;
|
||||
// ConstructUses? _contentCache;
|
||||
|
||||
ConstructEvent({required Event event}) {
|
||||
if (event.type != PangeaEventTypes.vocab) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a StudentAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
_event = event;
|
||||
}
|
||||
// ConstructEvent({required Event event}) {
|
||||
// if (event.type != PangeaEventTypes.vocab) {
|
||||
// throw Exception(
|
||||
// "${event.type} should not be used to make a StudentAnalyticsEvent",
|
||||
// );
|
||||
// }
|
||||
// _event = event;
|
||||
// }
|
||||
|
||||
Event get event => _event;
|
||||
// Event get event => _event;
|
||||
|
||||
ConstructUses get content {
|
||||
_contentCache ??= ConstructUses.fromJson(event.content);
|
||||
if (_contentCache!.lemma.isEmpty) {
|
||||
_contentCache!.lemma = event.stateKey!;
|
||||
}
|
||||
return _contentCache!;
|
||||
}
|
||||
// ConstructUses get content {
|
||||
// _contentCache ??= ConstructUses.fromJson(event.content);
|
||||
// if (_contentCache!.lemma.isEmpty) {
|
||||
// _contentCache!.lemma = event.stateKey!;
|
||||
// }
|
||||
// return _contentCache!;
|
||||
// }
|
||||
|
||||
void addAll(List<OneConstructUse> uses) {
|
||||
content.uses.addAll(uses);
|
||||
event.content = content.toJson();
|
||||
}
|
||||
// void addAll(List<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// if (content.uses.any((element) => element.id == use.id)) {
|
||||
// continue;
|
||||
// }
|
||||
// debugPrint("${use.toJson()}");
|
||||
// content.uses.add(use);
|
||||
// }
|
||||
// event.content = content.toJson();
|
||||
// }
|
||||
|
||||
Future<void> removeEdittedUses(
|
||||
List<String> removeIds,
|
||||
Client client,
|
||||
) async {
|
||||
_contentCache ??= ConstructUses.fromJson(event.content);
|
||||
if (_contentCache == null || _event.stateKey == null) return;
|
||||
final previousLength = _contentCache!.uses.length;
|
||||
_contentCache!.uses.removeWhere(
|
||||
(element) => removeIds.contains(element.msgId),
|
||||
);
|
||||
if (previousLength > _contentCache!.uses.length) {
|
||||
await client.setRoomStateWithKey(
|
||||
_event.room.id,
|
||||
_event.type,
|
||||
_event.stateKey!,
|
||||
_contentCache!.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Future<void> removeEdittedUses(
|
||||
// List<String> removeIds,
|
||||
// Client client,
|
||||
// ) async {
|
||||
// _contentCache ??= ConstructUses.fromJson(event.content);
|
||||
// if (_contentCache == null || _event.stateKey == null) return;
|
||||
// final previousLength = _contentCache!.uses.length;
|
||||
// _contentCache!.uses.removeWhere(
|
||||
// (element) => removeIds.contains(element.msgId),
|
||||
// );
|
||||
// if (previousLength > _contentCache!.uses.length) {
|
||||
// await client.setRoomStateWithKey(
|
||||
// _event.room.id,
|
||||
// _event.type,
|
||||
// _event.stateKey!,
|
||||
// _contentCache!.toJson(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
28
lib/pangea/models/analytics_event.dart
Normal file
28
lib/pangea/models/analytics_event.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
abstract class AnalyticsEvent {
|
||||
late Event _event;
|
||||
AnalyticsModel? contentCache;
|
||||
|
||||
AnalyticsEvent({required Event event}) {
|
||||
_event = event;
|
||||
}
|
||||
|
||||
Event get event => _event;
|
||||
|
||||
AnalyticsModel get content {
|
||||
switch (_event.type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
break;
|
||||
}
|
||||
return contentCache!;
|
||||
}
|
||||
}
|
||||
11
lib/pangea/models/analytics_model.dart
Normal file
11
lib/pangea/models/analytics_model.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
abstract class AnalyticsModel {
|
||||
DateTime? lastUpdated;
|
||||
String? prevEventId;
|
||||
DateTime? prevLastUpdated;
|
||||
|
||||
AnalyticsModel({
|
||||
this.lastUpdated,
|
||||
this.prevEventId,
|
||||
this.prevLastUpdated,
|
||||
});
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
||||
import '../constants/choreo_constants.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
import 'constructs_analytics_model.dart';
|
||||
|
|
@ -210,9 +211,12 @@ class ChoreoRecord {
|
|||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> toGrammarConstructUse(String msgId, String chatId) {
|
||||
List<OneConstructUse> toGrammarConstructUse(
|
||||
String msgId,
|
||||
String chatId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final DateTime now = DateTime.now();
|
||||
for (final step in choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
|
|
@ -222,11 +226,12 @@ class ChoreoRecord {
|
|||
OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
chatId: chatId,
|
||||
timeStamp: now,
|
||||
timeStamp: timestamp,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.grammar,
|
||||
id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ class OneConstructUse {
|
|||
String chatId;
|
||||
String? msgId;
|
||||
DateTime timeStamp;
|
||||
String? id;
|
||||
|
||||
OneConstructUse({
|
||||
required this.useType,
|
||||
|
|
@ -162,6 +163,7 @@ class OneConstructUse {
|
|||
required this.form,
|
||||
required this.msgId,
|
||||
required this.constructType,
|
||||
this.id,
|
||||
});
|
||||
|
||||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -176,6 +178,7 @@ class OneConstructUse {
|
|||
constructType: json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
: null,
|
||||
id: json['id'],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +194,7 @@ class OneConstructUse {
|
|||
if (!condensed && constructType != null) {
|
||||
data['constructType'] = constructType!.string;
|
||||
}
|
||||
if (id != null) data['id'] = id;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
52
lib/pangea/models/constructs_event.dart
Normal file
52
lib/pangea/models/constructs_event.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:fluffychat/pangea/models/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class ConstructAnalyticsEvent extends AnalyticsEvent {
|
||||
ConstructAnalyticsEvent({required Event event}) : super(event: event) {
|
||||
if (event.type != PangeaEventTypes.construct) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a ConstructAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ConstructAnalyticsModel get content {
|
||||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as ConstructAnalyticsModel;
|
||||
}
|
||||
|
||||
// void addAll(List<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// if (content.uses.any((element) => element.id == use.id)) {
|
||||
// continue;
|
||||
// }
|
||||
// debugPrint("${use.toJson()}");
|
||||
// content.uses.add(use);
|
||||
// }
|
||||
// event.content = content.toJson();
|
||||
// }
|
||||
|
||||
// Future<void> removeEdittedUses(
|
||||
// List<String> removeIds,
|
||||
// Client client,
|
||||
// ) async {
|
||||
// _contentCache ??= ConstructUses.fromJson(event.content);
|
||||
// if (_contentCache == null || _event.stateKey == null) return;
|
||||
// final previousLength = _contentCache!.uses.length;
|
||||
// _contentCache!.uses.removeWhere(
|
||||
// (element) => removeIds.contains(element.msgId),
|
||||
// );
|
||||
// if (previousLength > _contentCache!.uses.length) {
|
||||
// await client.setRoomStateWithKey(
|
||||
// _event.room.id,
|
||||
// _event.type,
|
||||
// _event.stateKey!,
|
||||
// _contentCache!.toJson(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
257
lib/pangea/models/constructs_model.dart
Normal file
257
lib/pangea/models/constructs_model.dart
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../enum/construct_type_enum.dart';
|
||||
|
||||
class ConstructAnalyticsModel extends AnalyticsModel {
|
||||
ConstructType type;
|
||||
List<LemmaConstructsModel> uses;
|
||||
|
||||
ConstructAnalyticsModel({
|
||||
required this.type,
|
||||
this.uses = const [],
|
||||
super.lastUpdated,
|
||||
super.prevEventId,
|
||||
super.prevLastUpdated,
|
||||
});
|
||||
|
||||
static const _lastUpdatedKey = "lupt";
|
||||
static const _usesKey = "uses";
|
||||
|
||||
factory ConstructAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
// try {
|
||||
// debugger(
|
||||
// when:
|
||||
// kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null),
|
||||
// );
|
||||
return ConstructAnalyticsModel(
|
||||
// lemma: json[ModelKey.lemma],
|
||||
// uses: (json['uses'] as Iterable)
|
||||
// .map<OneConstructUse?>(
|
||||
// (use) => use != null ? OneConstructUse.fromJson(use) : null,
|
||||
// )
|
||||
// .where((element) => element != null)
|
||||
// .cast<OneConstructUse>()
|
||||
// .toList(),
|
||||
type: ConstructTypeUtil.fromString(json['type']),
|
||||
lastUpdated: json[_lastUpdatedKey] != null
|
||||
? DateTime.parse(json[_lastUpdatedKey])
|
||||
: null,
|
||||
prevEventId: json[ModelKey.prevEventId],
|
||||
prevLastUpdated: json[ModelKey.prevLastUpdated] != null
|
||||
? DateTime.parse(json[ModelKey.prevLastUpdated])
|
||||
: null,
|
||||
uses: json[_usesKey]
|
||||
.values
|
||||
.map((lemmaUses) => LemmaConstructsModel.fromJson(lemmaUses))
|
||||
.cast<LemmaConstructsModel>()
|
||||
.toList(),
|
||||
);
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
toJson() {
|
||||
final Map<String, dynamic> usesMap = {};
|
||||
for (final use in uses) {
|
||||
debugPrint("use: $use");
|
||||
usesMap[use.lemma] = use.toJson();
|
||||
}
|
||||
|
||||
return {
|
||||
// ModelKey.lemma: lemma,
|
||||
// 'uses': uses.map((use) => use.toJson()).toList(),
|
||||
'type': type.string,
|
||||
_lastUpdatedKey: lastUpdated?.toIso8601String(),
|
||||
_usesKey: usesMap,
|
||||
ModelKey.prevEventId: prevEventId,
|
||||
ModelKey.prevLastUpdated: prevLastUpdated?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// void addUsesByUseType(List<OneConstructUse> uses) {
|
||||
// for (final use in uses) {
|
||||
// if (use.lemma != lemma) {
|
||||
// throw Exception('lemma mismatch');
|
||||
// }
|
||||
// uses.add(use);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
class LemmaConstructsModel {
|
||||
String lemma;
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
LemmaConstructsModel({
|
||||
required this.lemma,
|
||||
this.uses = const [],
|
||||
});
|
||||
|
||||
factory LemmaConstructsModel.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaConstructsModel(
|
||||
lemma: json[ModelKey.lemma],
|
||||
uses: (json['uses'] ?? [] as Iterable)
|
||||
.map<OneConstructUse?>(
|
||||
(use) => use != null ? OneConstructUse.fromJson(use) : null,
|
||||
)
|
||||
.where((element) => element != null)
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
ModelKey.lemma: lemma,
|
||||
'uses': uses.map((use) => use.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ConstructUseType {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
wa,
|
||||
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
|
||||
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
|
||||
ga,
|
||||
|
||||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
corIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
ignIGC,
|
||||
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
}
|
||||
|
||||
extension on ConstructUseType {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return 'ga';
|
||||
case ConstructUseType.wa:
|
||||
return 'wa';
|
||||
case ConstructUseType.corIt:
|
||||
return 'corIt';
|
||||
case ConstructUseType.incIt:
|
||||
return 'incIt';
|
||||
case ConstructUseType.ignIt:
|
||||
return 'ignIt';
|
||||
case ConstructUseType.ignIGC:
|
||||
return 'ignIGC';
|
||||
case ConstructUseType.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseType.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseType.unk:
|
||||
return 'unk';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return Icons.check;
|
||||
case ConstructUseType.wa:
|
||||
return Icons.thumb_up_sharp;
|
||||
case ConstructUseType.corIt:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.unk:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// class OneConstructUse {
|
||||
// String? lemma;
|
||||
// ConstructType? constructType;
|
||||
// String? form;
|
||||
// ConstructUseType useType;
|
||||
// String chatId;
|
||||
// String? msgId;
|
||||
// DateTime timeStamp;
|
||||
// String? id;
|
||||
|
||||
// OneConstructUse({
|
||||
// required this.useType,
|
||||
// required this.chatId,
|
||||
// required this.timeStamp,
|
||||
// required this.lemma,
|
||||
// required this.form,
|
||||
// required this.msgId,
|
||||
// required this.constructType,
|
||||
// this.id,
|
||||
// });
|
||||
|
||||
// factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
// return OneConstructUse(
|
||||
// useType: ConstructUseType.values
|
||||
// .firstWhere((e) => e.string == json['useType']),
|
||||
// chatId: json['chatId'],
|
||||
// timeStamp: DateTime.parse(json['timeStamp']),
|
||||
// lemma: json['lemma'],
|
||||
// form: json['form'],
|
||||
// msgId: json['msgId'],
|
||||
// constructType: json['constructType'] != null
|
||||
// ? ConstructTypeUtil.fromString(json['constructType'])
|
||||
// : null,
|
||||
// id: json['id'],
|
||||
// );
|
||||
// }
|
||||
|
||||
// Map<String, dynamic> toJson([bool condensed = true]) {
|
||||
// final Map<String, String?> data = {
|
||||
// 'useType': useType.string,
|
||||
// 'chatId': chatId,
|
||||
// 'timeStamp': timeStamp.toIso8601String(),
|
||||
// 'form': form,
|
||||
// 'msgId': msgId,
|
||||
// };
|
||||
// if (!condensed && lemma != null) data['lemma'] = lemma!;
|
||||
// if (!condensed && constructType != null) {
|
||||
// data['constructType'] = constructType!.string;
|
||||
// }
|
||||
// if (id != null) data['id'] = id;
|
||||
|
||||
// return data;
|
||||
// }
|
||||
|
||||
// Room? getRoom(Client client) {
|
||||
// return client.getRoomById(chatId);
|
||||
// }
|
||||
|
||||
// Future<Event?> getEvent(Client client) async {
|
||||
// final Room? room = getRoom(client);
|
||||
// if (room == null || msgId == null) return null;
|
||||
// return room.getEventById(msgId!);
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,161 +1,164 @@
|
|||
import 'dart:developer';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/client_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
// import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import 'chart_analytics_model.dart';
|
||||
// import '../constants/pangea_event_types.dart';
|
||||
// import 'chart_analytics_model.dart';
|
||||
|
||||
class StudentAnalyticsEvent {
|
||||
late Event _event;
|
||||
StudentAnalyticsSummary? _contentCache;
|
||||
List<RecentMessageRecord> _messagesToSave = [];
|
||||
// class StudentAnalyticsEvent {
|
||||
// late Event _event;
|
||||
// StudentAnalyticsSummary? _contentCache;
|
||||
// List<RecentMessageRecord> _messagesToSave = [];
|
||||
|
||||
StudentAnalyticsEvent({required Event event}) {
|
||||
if (event.type != PangeaEventTypes.studentAnalyticsSummary) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a StudentAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
_event = event;
|
||||
if (!classRoom.isSpace) {
|
||||
throw Exception(
|
||||
"non-class room should not be used to make a StudentAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
_event = event;
|
||||
// StudentAnalyticsEvent({required Event event}) {
|
||||
// if (event.type != PangeaEventTypes.studentAnalyticsSummary) {
|
||||
// throw Exception(
|
||||
// "${event.type} should not be used to make a StudentAnalyticsEvent",
|
||||
// );
|
||||
// }
|
||||
// _event = event;
|
||||
// _messagesToSave = [];
|
||||
// }
|
||||
|
||||
_messagesToSave = [];
|
||||
}
|
||||
// Event get event => _event;
|
||||
|
||||
Room get classRoom => _event.room;
|
||||
// StudentAnalyticsSummary get content {
|
||||
// _contentCache ??= StudentAnalyticsSummary.fromJson(event.content);
|
||||
// return _contentCache!;
|
||||
// }
|
||||
|
||||
Event get event => _event;
|
||||
// Future<void> removeEdittedMessages(
|
||||
// RecentMessageRecord message,
|
||||
// ) async {
|
||||
// final List<String> removeIds = await event.room.client.getEditHistory(
|
||||
// message.chatId,
|
||||
// message.eventId,
|
||||
// );
|
||||
// if (removeIds.isEmpty) return;
|
||||
// _messagesToSave.removeWhere(
|
||||
// (msg) => removeIds.any((e) => e == msg.eventId),
|
||||
// );
|
||||
// content.removeEdittedMessages(
|
||||
// event.room.client,
|
||||
// removeIds,
|
||||
// );
|
||||
// }
|
||||
|
||||
StudentAnalyticsSummary get content {
|
||||
_contentCache ??= StudentAnalyticsSummary.fromJson(event.content);
|
||||
return _contentCache!;
|
||||
}
|
||||
// // Future<void> handleNewMessage(
|
||||
// // RecentMessageRecord message, {
|
||||
// // isEdit = false,
|
||||
// // }) async {
|
||||
// // if (isEdit) {
|
||||
// // await removeEdittedMessages(message);
|
||||
// // }
|
||||
// // // _addMessage(message);
|
||||
// // _messagesToSave.add(message);
|
||||
// // debugPrint("messages to save is now: ${_messagesToSave.length}");
|
||||
|
||||
Future<void> removeEdittedMessages(
|
||||
RecentMessageRecord message,
|
||||
) async {
|
||||
final List<String> removeIds = await classRoom.client.getEditHistory(
|
||||
message.chatId,
|
||||
message.eventId,
|
||||
);
|
||||
if (removeIds.isEmpty) return;
|
||||
_messagesToSave.removeWhere(
|
||||
(msg) => removeIds.any((e) => e == msg.eventId),
|
||||
);
|
||||
content.removeEdittedMessages(
|
||||
classRoom.client,
|
||||
removeIds,
|
||||
);
|
||||
}
|
||||
// // if (DateTime.now().difference(content.lastUpdated).inMinutes >
|
||||
// // ClassDefaultValues.minutesDelayToUpdateMyAnalytics) {
|
||||
// // _updateStudentAnalytics();
|
||||
// // }
|
||||
// // }
|
||||
|
||||
Future<void> handleNewMessage(
|
||||
RecentMessageRecord message, {
|
||||
isEdit = false,
|
||||
}) async {
|
||||
if (classRoom.client.userID != _event.stateKey) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "should not be in handleNewMessage ${classRoom.client.userID} != ${_event.stateKey}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// // Future<void> bulkUpdate(List<RecentMessageRecord> messages) async {
|
||||
// // // if (event.room.client.userID != _event.stateKey) {
|
||||
// // // debugger(when: kDebugMode);
|
||||
// // // ErrorHandler.logError(
|
||||
// // // m: "should not be in bulkUpdate ${event.room.client.userID} != ${_event.stateKey}",
|
||||
// // // );
|
||||
// // // return;
|
||||
// // // }
|
||||
// // for (final message in messages) {
|
||||
// // await removeEdittedMessages(message);
|
||||
// // }
|
||||
|
||||
if (isEdit) {
|
||||
await removeEdittedMessages(message);
|
||||
}
|
||||
_addMessage(message);
|
||||
// // _messagesToSave.addAll(messages);
|
||||
// // await _updateStudentAnalytics();
|
||||
// // }
|
||||
|
||||
if (DateTime.now().difference(content.lastUpdated).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToUpdateMyAnalytics) {
|
||||
_updateStudentAnalytics();
|
||||
}
|
||||
}
|
||||
// // Future<void> _updateStudentAnalytics() async {
|
||||
// // content.lastUpdated = DateTime.now();
|
||||
// // content.addAll(_messagesToSave);
|
||||
// // _clearMessages();
|
||||
|
||||
Future<void> bulkUpdate(List<RecentMessageRecord> messages) async {
|
||||
if (classRoom.client.userID != _event.stateKey) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "should not be in bulkUpdate ${classRoom.client.userID} != ${_event.stateKey}",
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (final message in messages) {
|
||||
await removeEdittedMessages(message);
|
||||
}
|
||||
// // await event.room.client.setRoomStateWithKey(
|
||||
// // event.room.id,
|
||||
// // _event.type,
|
||||
// // '',
|
||||
// // content.toJson(),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
_messagesToSave.addAll(messages);
|
||||
_updateStudentAnalytics();
|
||||
}
|
||||
// Future<void> updateStudentAnalytics() async {
|
||||
// content.lastUpdated = DateTime.now();
|
||||
// // content.addAll(_messagesToSave);
|
||||
// // _clearMessages();
|
||||
|
||||
Future<void> _updateStudentAnalytics() async {
|
||||
content.lastUpdated = DateTime.now();
|
||||
content.addAll(_messagesToSave);
|
||||
debugPrint("updating student analytics");
|
||||
_clearMessages();
|
||||
// await event.room.client.setRoomStateWithKey(
|
||||
// event.room.id,
|
||||
// _event.type,
|
||||
// '',
|
||||
// content.toJson(),
|
||||
// );
|
||||
// await event.room.postLoad();
|
||||
// }
|
||||
|
||||
await classRoom.client.setRoomStateWithKey(
|
||||
classRoom.id,
|
||||
_event.type,
|
||||
_event.stateKey!,
|
||||
content.toJson(),
|
||||
);
|
||||
}
|
||||
// _addMessage(RecentMessageRecord message) {
|
||||
// if (_messagesToSave.every((e) => e.eventId != message.eventId)) {
|
||||
// _messagesToSave.add(message);
|
||||
// } else {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(
|
||||
// m: "adding message twice in StudentAnalyticsEvent._addMessage",
|
||||
// );
|
||||
// }
|
||||
// //PTODO - save to local storagge
|
||||
// }
|
||||
|
||||
_addMessage(RecentMessageRecord message) {
|
||||
if (_messagesToSave.every((e) => e.eventId != message.eventId)) {
|
||||
_messagesToSave.add(message);
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "adding message twice in StudentAnalyticsEvent._addMessage",
|
||||
);
|
||||
}
|
||||
//PTODO - save to local storagge
|
||||
}
|
||||
// _clearMessages() {
|
||||
// _messagesToSave.clear();
|
||||
// //PTODO - clear local storagge
|
||||
// }
|
||||
|
||||
_clearMessages() {
|
||||
_messagesToSave.clear();
|
||||
//PTODO - clear local storagge
|
||||
}
|
||||
// Future<TimeSeriesTotals> getTotals(String? chatId) async {
|
||||
// final TimeSeriesTotals totals = TimeSeriesTotals.empty;
|
||||
// final msgs = chatId == null
|
||||
// ? content.messages
|
||||
// : content.messages.where((msg) => msg.chatId == chatId);
|
||||
// for (final msg in msgs) {
|
||||
// totals.increment(msg);
|
||||
// }
|
||||
// return totals;
|
||||
// }
|
||||
|
||||
Future<TimeSeriesTotals> getTotals(String? chatId) async {
|
||||
final TimeSeriesTotals totals = TimeSeriesTotals.empty;
|
||||
final msgs = chatId == null
|
||||
? content.messages
|
||||
: content.messages.where((msg) => msg.chatId == chatId);
|
||||
for (final msg in msgs) {
|
||||
totals.increment(msg);
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
// Future<TimeSeriesInterval> getTimeServiesInterval(
|
||||
// DateTime start,
|
||||
// DateTime end,
|
||||
// String? chatId,
|
||||
// ) async {
|
||||
// final TimeSeriesInterval interval = TimeSeriesInterval(
|
||||
// start: start,
|
||||
// end: end,
|
||||
// totals: TimeSeriesTotals.empty,
|
||||
// );
|
||||
// for (final msg in content.messages) {
|
||||
// if (msg.time.isAfter(start) &&
|
||||
// msg.time.isBefore(end) &&
|
||||
// (chatId == null || chatId == msg.chatId)) {
|
||||
// interval.totals.increment(msg);
|
||||
// }
|
||||
// }
|
||||
// return interval;
|
||||
// }
|
||||
|
||||
Future<TimeSeriesInterval> getTimeServiesInterval(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
String? chatId,
|
||||
) async {
|
||||
final TimeSeriesInterval interval = TimeSeriesInterval(
|
||||
start: start,
|
||||
end: end,
|
||||
totals: TimeSeriesTotals.empty,
|
||||
);
|
||||
for (final msg in content.messages) {
|
||||
if (msg.time.isAfter(start) &&
|
||||
msg.time.isBefore(end) &&
|
||||
(chatId == null || chatId == msg.chatId)) {
|
||||
interval.totals.increment(msg);
|
||||
}
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
// bool isAlreadyAdded(RecentMessageRecord message) {
|
||||
// return content.messages.any(
|
||||
// (element) => element.eventId == message.eventId,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class RecentMessageRecord {
|
|||
|
||||
class StudentAnalyticsSummary {
|
||||
late List<RecentMessageRecord> _messages;
|
||||
DateTime lastUpdated;
|
||||
DateTime? lastUpdated;
|
||||
|
||||
StudentAnalyticsSummary({
|
||||
required List<RecentMessageRecord> messages,
|
||||
|
|
@ -69,11 +69,7 @@ class StudentAnalyticsSummary {
|
|||
|
||||
void addAll(List<RecentMessageRecord> msgs) {
|
||||
for (final msg in msgs) {
|
||||
if (_messages.any((element) => element.eventId == msg.eventId)) {
|
||||
ErrorHandler.logError(
|
||||
m: "adding message twice in StudentAnalyticsSummary.add",
|
||||
);
|
||||
} else {
|
||||
if (!(_messages.any((element) => element.eventId == msg.eventId))) {
|
||||
_messages.add(msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +88,7 @@ class StudentAnalyticsSummary {
|
|||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
_lastUpdatedKey: lastUpdated.toIso8601String(),
|
||||
_lastUpdatedKey: lastUpdated?.toIso8601String(),
|
||||
};
|
||||
|
||||
factory StudentAnalyticsSummary.fromJson(json) {
|
||||
|
|
@ -110,7 +106,9 @@ class StudentAnalyticsSummary {
|
|||
}
|
||||
return StudentAnalyticsSummary(
|
||||
messages: savedMessages,
|
||||
lastUpdated: DateTime.parse(json[_lastUpdatedKey]),
|
||||
lastUpdated: json[_lastUpdatedKey] != null
|
||||
? DateTime.parse(json[_lastUpdatedKey])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
lib/pangea/models/summary_analytics_event.dart
Normal file
21
lib/pangea/models/summary_analytics_event.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:fluffychat/pangea/models/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class SummaryAnalyticsEvent extends AnalyticsEvent {
|
||||
SummaryAnalyticsEvent({required Event event}) : super(event: event) {
|
||||
if (event.type != PangeaEventTypes.summaryAnalytics) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a SummaryAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
SummaryAnalyticsModel get content {
|
||||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as SummaryAnalyticsModel;
|
||||
}
|
||||
}
|
||||
57
lib/pangea/models/summary_analytics_model.dart
Normal file
57
lib/pangea/models/summary_analytics_model.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SummaryAnalyticsModel extends AnalyticsModel {
|
||||
late List<RecentMessageRecord> _messages;
|
||||
|
||||
SummaryAnalyticsModel({
|
||||
required List<RecentMessageRecord> messages,
|
||||
super.lastUpdated,
|
||||
super.prevEventId,
|
||||
super.prevLastUpdated,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
static const _lastUpdatedKey = "lupt";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
_lastUpdatedKey: lastUpdated?.toIso8601String(),
|
||||
ModelKey.prevEventId: prevEventId,
|
||||
ModelKey.prevLastUpdated: prevLastUpdated?.toIso8601String(),
|
||||
};
|
||||
|
||||
factory SummaryAnalyticsModel.fromJson(json) {
|
||||
List<RecentMessageRecord> savedMessages = [];
|
||||
try {
|
||||
savedMessages = json[_messagesKey] != null
|
||||
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
|
||||
.map((e) => RecentMessageRecord.fromJson(e))
|
||||
.toList()
|
||||
.cast<RecentMessageRecord>()
|
||||
: [];
|
||||
} catch (err, stack) {
|
||||
if (kDebugMode) rethrow;
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
return SummaryAnalyticsModel(
|
||||
messages: savedMessages,
|
||||
lastUpdated: json[_lastUpdatedKey] != null
|
||||
? DateTime.parse(json[_lastUpdatedKey])
|
||||
: null,
|
||||
prevEventId: json[ModelKey.prevEventId],
|
||||
prevLastUpdated: json[ModelKey.prevLastUpdated] != null
|
||||
? DateTime.parse(json[ModelKey.prevLastUpdated])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -14,115 +17,135 @@ import 'list_summary_analytics.dart';
|
|||
class AnalyticsListTile extends StatefulWidget {
|
||||
const AnalyticsListTile({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.displayName,
|
||||
required this.avatar,
|
||||
required this.type,
|
||||
required this.id,
|
||||
required this.allowNavigateOnSelect,
|
||||
required this.defaultSelected,
|
||||
required this.selected,
|
||||
required this.avatar,
|
||||
required this.allowNavigateOnSelect,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
required this.pangeaController,
|
||||
// this.isEnabled = true,
|
||||
this.showSpaceAnalytics = true,
|
||||
this.refreshStream,
|
||||
});
|
||||
|
||||
final Uri? avatar;
|
||||
final String displayName;
|
||||
final AnalyticsEntryType type;
|
||||
final String id;
|
||||
final ChartAnalyticsModel? model;
|
||||
final bool allowNavigateOnSelect;
|
||||
final void Function(AnalyticsSelected) onTap;
|
||||
final bool selected;
|
||||
final bool enabled;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected selected;
|
||||
|
||||
final Uri? avatar;
|
||||
|
||||
final bool allowNavigateOnSelect;
|
||||
final bool isSelected;
|
||||
// final bool isEnabled;
|
||||
final bool showSpaceAnalytics;
|
||||
|
||||
final PangeaController pangeaController;
|
||||
final StreamController? refreshStream;
|
||||
|
||||
@override
|
||||
AnalyticsListTileState createState() => AnalyticsListTileState();
|
||||
}
|
||||
|
||||
class AnalyticsListTileState extends State<AnalyticsListTile> {
|
||||
ChartAnalyticsModel? tileData;
|
||||
StreamSubscription? refreshSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setTileData();
|
||||
refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) {
|
||||
setTileData(forceUpdate: forceUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> setTileData({forceUpdate = false}) async {
|
||||
tileData = await MatrixState.pangeaController.analytics.getAnalytics(
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Room? room = Matrix.of(context).client.getRoomById(widget.id);
|
||||
final Room? room =
|
||||
Matrix.of(context).client.getRoomById(widget.selected.id);
|
||||
return Material(
|
||||
color: widget.selected
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
child: Opacity(
|
||||
opacity: widget.enabled ? 1 : 0.5,
|
||||
child: Tooltip(
|
||||
message: widget.enabled
|
||||
? ""
|
||||
: widget.type == AnalyticsEntryType.room
|
||||
? L10n.of(context)!.joinToView
|
||||
: L10n.of(context)!.studentAnalyticsNotAvailable,
|
||||
child: ListTile(
|
||||
leading: widget.type == AnalyticsEntryType.privateChats
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.forum),
|
||||
)
|
||||
: Avatar(
|
||||
mxContent: widget.avatar,
|
||||
name: widget.displayName,
|
||||
littleIcon: room?.roomTypeIcon,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.displayName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
),
|
||||
child: Tooltip(
|
||||
message: widget.selected.type == AnalyticsEntryType.room
|
||||
? L10n.of(context)!.joinToView
|
||||
: L10n.of(context)!.studentAnalyticsNotAvailable,
|
||||
child: ListTile(
|
||||
leading: widget.selected.type == AnalyticsEntryType.privateChats
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.forum),
|
||||
)
|
||||
: Avatar(
|
||||
mxContent: widget.avatar,
|
||||
name: widget.selected.displayName,
|
||||
littleIcon: room?.roomTypeIcon,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.selected.displayName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.timeOfLastMessage,
|
||||
child: Text(
|
||||
widget.model?.lastMessage?.localizedTimeShort(context) ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.timeOfLastMessage,
|
||||
child: Text(
|
||||
tileData?.lastMessage?.localizedTimeShort(context) ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: widget.showSpaceAnalytics || !(room?.isSpace ?? false)
|
||||
? ListSummaryAnalytics(
|
||||
chartAnalytics: widget.model,
|
||||
)
|
||||
: null,
|
||||
selected: widget.selected,
|
||||
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
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: widget.showSpaceAnalytics || !(room?.isSpace ?? false)
|
||||
? ListSummaryAnalytics(
|
||||
chartAnalytics: tileData,
|
||||
)
|
||||
: null,
|
||||
selected: widget.isSelected,
|
||||
onTap: () {
|
||||
(room?.isSpace ?? false) && widget.allowNavigateOnSelect
|
||||
? context.go(
|
||||
'/rooms/analytics/${room!.id}',
|
||||
)
|
||||
: widget.onTap(widget.selected);
|
||||
},
|
||||
trailing: (room?.isSpace ?? false) &&
|
||||
widget.selected.type != AnalyticsEntryType.privateChats &&
|
||||
widget.allowNavigateOnSelect
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
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';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../controllers/pangea_controller.dart';
|
||||
|
|
@ -17,7 +15,6 @@ import '../../models/chart_analytics_model.dart';
|
|||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final List<TabData> tabs;
|
||||
final Future Function(BuildContext) refreshData;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
|
|
@ -27,7 +24,6 @@ class BaseAnalyticsPage extends StatefulWidget {
|
|||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabs,
|
||||
required this.refreshData,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
this.myAnalyticsController,
|
||||
|
|
@ -42,53 +38,57 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
BarChartViewSelection? selectedView;
|
||||
AnalyticsSelected? selected;
|
||||
String? currentLemma;
|
||||
ChartAnalyticsModel? chartData;
|
||||
StreamController refreshStream = StreamController.broadcast();
|
||||
|
||||
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
|
||||
ChartAnalyticsModel? chartData(
|
||||
BuildContext context,
|
||||
AnalyticsSelected? selectedParam,
|
||||
) {
|
||||
final AnalyticsSelected analyticsSelected =
|
||||
selectedParam ?? widget.defaultSelected;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setChartData();
|
||||
}
|
||||
|
||||
if (analyticsSelected.type == AnalyticsEntryType.privateChats) {
|
||||
return pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: analyticsSelected.id,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
);
|
||||
}
|
||||
Future<void> onRefresh() async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
debugPrint("updating analytics");
|
||||
await pangeaController.myAnalytics.updateAnalytics();
|
||||
await setChartData(forceUpdate: true);
|
||||
refreshStream.add(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String? chatId = analyticsSelected.type == AnalyticsEntryType.room
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
Future<ChartAnalyticsModel> fetchChartData(
|
||||
AnalyticsSelected? params, {
|
||||
forceUpdate = false,
|
||||
}) async {
|
||||
ChartAnalyticsModel? data = pangeaController.analytics.getAnalyticsLocal(
|
||||
timeSpan: currentTimeSpan,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: params,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
|
||||
String? studentId = analyticsSelected.type == AnalyticsEntryType.student
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
String? classId = analyticsSelected.type == AnalyticsEntryType.space
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
classId ??= widget.alwaysSelected?.type == AnalyticsEntryType.space
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
final data = pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: classId,
|
||||
chatId: chatId,
|
||||
studentId: studentId,
|
||||
data ??= await pangeaController.analytics.getAnalytics(
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: params,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> setChartData({forceUpdate = false}) async {
|
||||
final ChartAnalyticsModel newData = await fetchChartData(
|
||||
selected,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
setState(() => chartData = newData);
|
||||
}
|
||||
|
||||
TimeSpan get currentTimeSpan =>
|
||||
pangeaController.analytics.currentAnalyticsTimeSpan;
|
||||
|
||||
|
|
@ -103,33 +103,13 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
}
|
||||
|
||||
Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
|
||||
final bool joinSelectedRoom =
|
||||
selectedParam.type == AnalyticsEntryType.room &&
|
||||
!enableSelection(
|
||||
selectedParam,
|
||||
);
|
||||
|
||||
if (joinSelectedRoom) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = Matrix.of(context).client.waitForRoomInSync(
|
||||
selectedParam.id,
|
||||
join: true,
|
||||
);
|
||||
await Matrix.of(context).client.joinRoom(selectedParam.id);
|
||||
await waitForRoom;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
currentLemma = null;
|
||||
selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
});
|
||||
|
||||
pangeaController.analytics.setConstructs(
|
||||
await pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
|
|
@ -141,54 +121,28 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
|
||||
Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
await widget.refreshData(context);
|
||||
await pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
removeIT: true,
|
||||
);
|
||||
await setChartData();
|
||||
setState(() {});
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
void setSelectedView(BarChartViewSelection? view) {
|
||||
currentLemma = null;
|
||||
selectedView = view;
|
||||
if (!enableSelection(selected)) {
|
||||
toggleSelection(selected!);
|
||||
}
|
||||
setState(() {});
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool enableSelection(AnalyticsSelected? selectedParam) {
|
||||
if (selectedView == BarChartViewSelection.grammar) {
|
||||
if (selectedParam?.type == AnalyticsEntryType.room) {
|
||||
return Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(selectedParam!.id)
|
||||
?.membership ==
|
||||
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;
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
switch (controller.selectedView!) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData(
|
||||
context,
|
||||
controller.selected,
|
||||
),
|
||||
chartAnalytics: controller.chartData,
|
||||
);
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
|
|
@ -41,6 +38,7 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
selected: controller.selected,
|
||||
controller: controller,
|
||||
pangeaController: controller.pangeaController,
|
||||
refreshStream: controller.refreshStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -98,19 +96,34 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: controller.navigate,
|
||||
),
|
||||
actions: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
],
|
||||
// actions: [
|
||||
// TimeSpanMenuButton(
|
||||
// value: controller.currentTimeSpan,
|
||||
// onChange: (TimeSpan value) =>
|
||||
// controller.toggleTimeSpan(context, value),
|
||||
// ),
|
||||
// ],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: controller.selectedView != null
|
||||
? Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: controller.onRefresh,
|
||||
tooltip: L10n.of(context)!.refresh,
|
||||
),
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: chartView(context),
|
||||
|
|
@ -153,28 +166,18 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
children: [
|
||||
...controller.widget.tabs[0].items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
"",
|
||||
),
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type:
|
||||
controller.widget.tabs[0].type,
|
||||
selected:
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
enabled: controller.enableSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
showSpaceAnalytics: false,
|
||||
onTap: (_) =>
|
||||
controller.toggleSelection(
|
||||
|
|
@ -188,35 +191,33 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
.widget
|
||||
.tabs[0]
|
||||
.allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
),
|
||||
),
|
||||
if (controller
|
||||
.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
avatar: null,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!
|
||||
.allPrivateChats,
|
||||
),
|
||||
selected: AnalyticsSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
),
|
||||
displayName: L10n.of(context)!
|
||||
.allPrivateChats,
|
||||
id: controller
|
||||
.widget.defaultSelected.id,
|
||||
type:
|
||||
AnalyticsEntryType.privateChats,
|
||||
allowNavigateOnSelect: false,
|
||||
selected: controller.isSelected(
|
||||
isSelected: controller.isSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -226,36 +227,25 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
children: controller.widget.tabs[1].items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller
|
||||
.widget.tabs[1].type,
|
||||
"",
|
||||
),
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[1].type,
|
||||
item.displayName,
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type: controller
|
||||
.widget.tabs[1].type,
|
||||
selected: controller
|
||||
isSelected: controller
|
||||
.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller
|
||||
.widget
|
||||
.tabs[1]
|
||||
.allowNavigateOnSelect,
|
||||
enabled:
|
||||
controller.enableSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller
|
||||
.widget.tabs[1].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
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';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
|
|
@ -16,14 +12,12 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import 'class_analytics_view.dart';
|
||||
|
||||
enum AnalyticsPageType { classList, student, classDetails }
|
||||
|
||||
class ClassAnalyticsPage extends StatefulWidget {
|
||||
// final AnalyticsPageType type;
|
||||
const ClassAnalyticsPage({super.key});
|
||||
|
||||
@override
|
||||
|
|
@ -31,48 +25,25 @@ class ClassAnalyticsPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
bool _initialized = false;
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
// StreamSubscription<Event>? stateSub;
|
||||
// Timer? refreshTimer;
|
||||
|
||||
List<SpaceRoomsChunk> chats = [];
|
||||
List<User> students = [];
|
||||
|
||||
String? get classId => GoRouterState.of(context).pathParameters['classid'];
|
||||
|
||||
Room? _classRoom;
|
||||
|
||||
Room? get classRoom {
|
||||
if (_classRoom == null || _classRoom!.id != classId) {
|
||||
debugPrint("updating _classRoom");
|
||||
_classRoom = classId != null
|
||||
? Matrix.of(context).client.getRoomById(classId!)
|
||||
: null;
|
||||
|
||||
getChatAndStudents()
|
||||
.then(
|
||||
(_) => _pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: AnalyticsSelected(
|
||||
classId!,
|
||||
AnalyticsEntryType.space,
|
||||
className(context),
|
||||
),
|
||||
removeIT: true,
|
||||
forceUpdate: true,
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(_) => getChatAndStudentAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
return _classRoom;
|
||||
}
|
||||
|
||||
String className(BuildContext context) {
|
||||
return classRoom?.name ?? "";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -80,14 +51,6 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
if (classRoom == null || (!(classRoom?.isSpace ?? false))) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
|
||||
stateSub = _pangeaController.matrixState.client.onRoomState.stream
|
||||
.where(
|
||||
(event) =>
|
||||
event.type == PangeaEventTypes.studentAnalyticsSummary &&
|
||||
event.roomId == classId,
|
||||
)
|
||||
.listen(onStateUpdate);
|
||||
getChatAndStudents();
|
||||
});
|
||||
}
|
||||
|
|
@ -122,21 +85,12 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
}
|
||||
}
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer?.isActive ?? false)) {
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => getChatAndStudentAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
}
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// refreshTimer?.cancel();
|
||||
// stateSub?.cancel();
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -146,57 +100,10 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
// but this is computationally expensive!
|
||||
// key: UniqueKey(),
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
onFinish: () {
|
||||
getChatAndStudentAnalytics(context);
|
||||
},
|
||||
// onFinish: () {
|
||||
// getChatAndStudentAnalytics(context);
|
||||
// },
|
||||
child: ClassAnalyticsView(this),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getChatAndStudentAnalytics(
|
||||
BuildContext context, [
|
||||
forceUpdate = false,
|
||||
]) async {
|
||||
try {
|
||||
if (classRoom == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(m: 'classroom should not be null');
|
||||
}
|
||||
final List<Future<ChartAnalyticsModel?>> analyticsFutures = [];
|
||||
for (final student in students) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: classRoom,
|
||||
studentId: student.id,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final chat in chats) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: classRoom,
|
||||
chatId: chat.roomId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: classRoom,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalyticsForPrivateChats(
|
||||
classRoom: classRoom,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
await Future.wait(analyticsFutures);
|
||||
if (mounted) setState(() {});
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,16 +50,15 @@ class ClassAnalyticsView extends StatelessWidget {
|
|||
? BaseAnalyticsPage(
|
||||
pageTitle: pageTitle,
|
||||
tabs: [tab1, tab2],
|
||||
refreshData: controller.getChatAndStudentAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.classId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.className(context),
|
||||
controller.classRoom?.name ?? "",
|
||||
),
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.classId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.className(context),
|
||||
controller.classRoom?.name ?? "",
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../constants/pangea_event_types.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../models/chart_analytics_model.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
|
|
@ -22,42 +22,6 @@ class AnalyticsClassList extends StatefulWidget {
|
|||
class AnalyticsClassListController extends State<AnalyticsClassList> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
List<ChartAnalyticsModel> models = [];
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Map<String, Timer> refreshTimer = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
stateSub = pangeaController.matrixState.client.onRoomState.stream
|
||||
.where(
|
||||
(event) => event.type == PangeaEventTypes.studentAnalyticsSummary,
|
||||
)
|
||||
.listen(onStateUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer[newState.room.id]?.isActive ?? false)) {
|
||||
refreshTimer[newState.room.id] = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
if (newState.room.isSpace) {
|
||||
updateClassAnalytics(context, newState.room);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
for (final timer in refreshTimer.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
stateSub?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -65,32 +29,32 @@ class AnalyticsClassListController extends State<AnalyticsClassList> {
|
|||
shimmerChild: const ListPlaceholder(),
|
||||
child: AnalyticsClassListView(this),
|
||||
onFinish: () {
|
||||
getAllClassAnalytics(context);
|
||||
// getAllClassAnalytics(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getAllClassAnalytics(BuildContext context) async {
|
||||
await pangeaController.analytics.allClassAnalytics();
|
||||
setState(() {
|
||||
debugPrint("class list post getAllClassAnalytics");
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateClassAnalytics(
|
||||
BuildContext context,
|
||||
Room classRoom,
|
||||
Future<ChartAnalyticsModel?> updateClassAnalytics(
|
||||
Room? space,
|
||||
) async {
|
||||
await pangeaController.analytics
|
||||
.getAnalytics(classRoom: classRoom, forceUpdate: true);
|
||||
setState(() {
|
||||
debugPrint("class list post updateClassAnalytics");
|
||||
});
|
||||
if (space == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final data = await pangeaController.analytics.getAnalytics(
|
||||
defaultSelected: AnalyticsSelected(
|
||||
space.id,
|
||||
AnalyticsEntryType.space,
|
||||
space.name,
|
||||
),
|
||||
forceUpdate: true,
|
||||
);
|
||||
setState(() {});
|
||||
return data;
|
||||
}
|
||||
|
||||
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
|
||||
pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
setState(() {});
|
||||
getAllClassAnalytics(context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,18 +50,23 @@ class AnalyticsClassListView extends StatelessWidget {
|
|||
builder: (context, snapshot) => ListView.builder(
|
||||
itemCount: snapshot.hasData ? snapshot.data?.length ?? 0 : 0,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
defaultSelected: AnalyticsSelected(
|
||||
snapshot.data![i].id,
|
||||
AnalyticsEntryType.space,
|
||||
"",
|
||||
),
|
||||
avatar: snapshot.data![i].avatar,
|
||||
model: controller.pangeaController.analytics
|
||||
.getAnalyticsLocal(classId: snapshot.data![i].id),
|
||||
displayName: snapshot.data![i].name,
|
||||
id: snapshot.data![i].id,
|
||||
type: AnalyticsEntryType.space,
|
||||
// selected: false,
|
||||
selected: AnalyticsSelected(
|
||||
snapshot.data![i].id,
|
||||
AnalyticsEntryType.space,
|
||||
snapshot.data![i].name,
|
||||
),
|
||||
onTap: (selected) => context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
),
|
||||
allowNavigateOnSelect: true,
|
||||
selected: false,
|
||||
isSelected: false,
|
||||
pangeaController: controller.pangeaController,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
|
|
@ -25,6 +24,7 @@ class ConstructList extends StatefulWidget {
|
|||
final AnalyticsSelected? selected;
|
||||
final BaseAnalyticsController controller;
|
||||
final PangeaController pangeaController;
|
||||
final StreamController refreshStream;
|
||||
|
||||
const ConstructList({
|
||||
super.key,
|
||||
|
|
@ -32,6 +32,7 @@ class ConstructList extends StatefulWidget {
|
|||
required this.defaultSelected,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.refreshStream,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ class ConstructListState extends State<ConstructList> {
|
|||
pangeaController: widget.pangeaController,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
refreshStream: widget.refreshStream,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -93,12 +95,12 @@ class ConstructListState extends State<ConstructList> {
|
|||
// subtitle = total uses, equal to construct.content.uses.length
|
||||
// list has a fixed height of 400 and is scrollable
|
||||
class ConstructListView extends StatefulWidget {
|
||||
// final List<ConstructEvent> constructs;
|
||||
final bool init;
|
||||
final BaseAnalyticsController controller;
|
||||
final PangeaController pangeaController;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? selected;
|
||||
final StreamController refreshStream;
|
||||
|
||||
const ConstructListView({
|
||||
super.key,
|
||||
|
|
@ -106,6 +108,7 @@ class ConstructListView extends StatefulWidget {
|
|||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.defaultSelected,
|
||||
required this.refreshStream,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
|
|
@ -118,55 +121,30 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
final Map<String, PangeaMessageEvent> _msgEventCache = {};
|
||||
final List<PangeaMessageEvent> _msgEvents = [];
|
||||
bool fetchingUses = false;
|
||||
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
StreamSubscription? refreshSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
stateSub = Matrix.of(context)
|
||||
.client
|
||||
.onRoomState
|
||||
.stream
|
||||
//could optimize here be determing if the vocab event is relevant for
|
||||
//currently displayed data
|
||||
.where((event) => event.type == PangeaEventTypes.vocab)
|
||||
.listen(onStateUpdate);
|
||||
}
|
||||
|
||||
Future<void> onStateUpdate(Event? newState) async {
|
||||
debugPrint("onStateUpdate construct list");
|
||||
if (refreshTimer?.isActive ?? false) return;
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() async {
|
||||
await widget.pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
);
|
||||
await fetchUses();
|
||||
},
|
||||
);
|
||||
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
|
||||
widget.pangeaController.analytics
|
||||
.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.then((_) => setState(() {}));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshSubscription?.cancel();
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
}
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(ConstructListView oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// fetchUses();
|
||||
// }
|
||||
|
||||
int get lemmaIndex =>
|
||||
constructs?.indexWhere(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
|
|
@ -241,16 +219,19 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
}
|
||||
}
|
||||
|
||||
List<AggregateConstructUses>? get constructs =>
|
||||
widget.pangeaController.analytics.constructs != null
|
||||
? widget.pangeaController.myAnalytics
|
||||
.aggregateConstructData(
|
||||
widget.pangeaController.analytics.constructs!,
|
||||
)
|
||||
.sorted(
|
||||
(a, b) => b.uses.length.compareTo(a.uses.length),
|
||||
)
|
||||
: null;
|
||||
List<AggregateConstructUses>? get constructs {
|
||||
if (widget.pangeaController.analytics.constructs == null) {
|
||||
return null;
|
||||
}
|
||||
return widget.pangeaController.myAnalytics
|
||||
.aggregateConstructData(widget.pangeaController.analytics.constructs!)
|
||||
.where((lemmaUses) => lemmaUses.uses.isNotEmpty)
|
||||
.sorted((a, b) {
|
||||
final int cmp = b.uses.length.compareTo(a.uses.length);
|
||||
if (cmp != 0) return cmp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
|
|
@ -302,6 +283,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if ((constructs?.isEmpty ?? true) ||
|
||||
(widget.controller.currentLemma != null && currentConstruct == null)) {
|
||||
return Expanded(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chart_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -11,7 +8,6 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../extensions/client_extension.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics_view.dart';
|
||||
|
|
@ -26,37 +22,44 @@ class StudentAnalyticsPage extends StatefulWidget {
|
|||
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
AnalyticsSelected? selected;
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
StreamSubscription? stateSub;
|
||||
|
||||
List<Room> _chats = [];
|
||||
List<Room> _spaces = [];
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer?.isActive ?? false)) {
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => getClassAndChatAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
stateSub = _pangeaController.myAnalytics.stateStream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
await getClassAndChatAnalytics(context);
|
||||
stateSub = _pangeaController.matrixState.client.onRoomState.stream
|
||||
.where(
|
||||
(event) =>
|
||||
event.type == PangeaEventTypes.studentAnalyticsSummary &&
|
||||
event.senderId == userId,
|
||||
)
|
||||
.listen(onStateUpdate);
|
||||
List<Room> get chats {
|
||||
if (_pangeaController.myAnalytics.studentChats.isEmpty) {
|
||||
_pangeaController.myAnalytics
|
||||
.setStudentChats()
|
||||
.then((_) => setState(() {}));
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentChats;
|
||||
}
|
||||
|
||||
List<Room> get spaces {
|
||||
if (_pangeaController.myAnalytics.studentSpaces.isEmpty) {
|
||||
_pangeaController.myAnalytics
|
||||
.setStudentSpaces()
|
||||
.then((_) => setState(() {}));
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentSpaces;
|
||||
}
|
||||
|
||||
String? get userId {
|
||||
final id = _pangeaController.matrixState.client.userID;
|
||||
debugger(when: kDebugMode && id == null);
|
||||
return id;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -66,96 +69,8 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
|||
// but this is computationally expensive!
|
||||
// key: UniqueKey(),
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
onFinish: initialize,
|
||||
// onFinish: initialize,
|
||||
child: StudentAnalyticsView(this),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getClassAndChatAnalytics(
|
||||
BuildContext context, [
|
||||
forceUpdate = false,
|
||||
]) async {
|
||||
final List<Future<ChartAnalyticsModel?>> analyticsFutures = [];
|
||||
for (final chat in (await getChats())) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
chatId: chat.id,
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final space in (await getSpaces())) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: space,
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
await Future.wait(analyticsFutures);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<List<Room>> getSpaces() async {
|
||||
final List<Room> rooms = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
setState(() => _spaces = rooms);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
List<Room>? get spaces {
|
||||
try {
|
||||
if (_spaces.isNotEmpty) return _spaces;
|
||||
getSpaces();
|
||||
return _spaces;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Room>> getChats() async {
|
||||
final List<String> teacherRoomIds =
|
||||
await Matrix.of(context).client.teacherRoomIds;
|
||||
_chats = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!teacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
setState(() => _chats = _chats);
|
||||
return _chats;
|
||||
}
|
||||
|
||||
List<Room>? get chats {
|
||||
try {
|
||||
if (_chats.isNotEmpty) return _chats;
|
||||
getChats();
|
||||
return _chats;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
String? get userId {
|
||||
final id = _pangeaController.matrixState.client.userID;
|
||||
debugger(when: kDebugMode && id == null);
|
||||
return id;
|
||||
}
|
||||
|
||||
String get username =>
|
||||
_pangeaController.matrixState.client.userID?.localpart ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
final TabData chatTabData = TabData(
|
||||
type: AnalyticsEntryType.room,
|
||||
icon: Icons.chat_bubble_outline,
|
||||
items: (controller.chats ?? [])
|
||||
items: (controller.chats)
|
||||
.map(
|
||||
(c) => TabItem(
|
||||
avatar: c.avatar,
|
||||
|
|
@ -47,7 +47,6 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
? BaseAnalyticsPage(
|
||||
pageTitle: pageTitle,
|
||||
tabs: [chatTabData, classTabData],
|
||||
refreshData: controller.getClassAndChatAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.userId!,
|
||||
AnalyticsEntryType.student,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../enum/time_span.dart';
|
||||
|
|
@ -16,6 +15,7 @@ class TimeSpanMenuButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<TimeSpan>(
|
||||
offset: const Offset(0, 100),
|
||||
icon: const Icon(Icons.calendar_month_outlined),
|
||||
tooltip: L10n.of(context)!.changeDateRange,
|
||||
initialValue: value,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/class_default_values.dart';
|
||||
import '../extensions/pangea_room_extension.dart';
|
||||
|
|
@ -16,7 +14,6 @@ class ClassChatPowerLevels {
|
|||
final Map<String, dynamic> powerLevelOverride = {};
|
||||
powerLevelOverride['events'] = {
|
||||
EventTypes.spaceChild: 0,
|
||||
PangeaEventTypes.studentAnalyticsSummary: 0,
|
||||
};
|
||||
powerLevelOverride['users'] = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ abstract class ClientManager {
|
|||
// #Pangea
|
||||
PangeaEventTypes.classSettings,
|
||||
PangeaEventTypes.rules,
|
||||
PangeaEventTypes.vocab,
|
||||
PangeaEventTypes.botOptions,
|
||||
EventTypes.RoomTopic,
|
||||
EventTypes.RoomAvatar,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue