Merge branch 'main' into only-replace-correct
This commit is contained in:
commit
cec3d7f710
79 changed files with 7344 additions and 6709 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -4651,5 +4651,29 @@
|
|||
"conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Enviar aviso de discusión cuando el usuario reacciona ⏩ al mensaje del bot.",
|
||||
"conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reacción al envío del aviso de debate",
|
||||
"studentAnalyticsNotAvailable": "Datos de los estudiantes no disponibles actualmente",
|
||||
"roomDataMissing": "Es posible que falten algunos datos de las salas de las que no es miembro."
|
||||
}
|
||||
"roomDataMissing": "Es posible que falten algunos datos de las salas de las que no es miembro.",
|
||||
"suggestToChat": "Sugerir este chat",
|
||||
"suggestToChatDesc": "Los chats sugeridos aparecerán en las listas de chats",
|
||||
"roomCapacity": "Capacidad de la sala",
|
||||
"roomFull": "Esta sala ya está al límite de su capacidad.",
|
||||
"topicNotSet": "El tema no se ha fijado.",
|
||||
"capacityNotSet": "Esta sala no tiene límite de capacidad.",
|
||||
"roomCapacityHasBeenChanged": "Capacidad de la sala modificada",
|
||||
"roomExceedsCapacity": "La sala supera su capacidad. Considere la posibilidad de retirar a los alumnos de la sala o de aumentar la capacidad.",
|
||||
"capacitySetTooLow": "La capacidad de la sala no puede fijarse por debajo del número actual de no administradores.",
|
||||
"roomCapacityExplanation": "La capacidad de la sala limita el número de personas que pueden entrar en ella.",
|
||||
"enterNumber": "Introduzca un valor numérico entero.",
|
||||
"autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística",
|
||||
"autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes",
|
||||
"runGrammarCorrection": "Corregir la gramática",
|
||||
"grammarCorrectionFailed": "Cuestiones a tratar",
|
||||
"grammarCorrectionComplete": "Corrección gramatical completa",
|
||||
"leaveRoomDescription": "El chat se moverá al archivo. Los demás usuarios podrán ver que has abandonado el chat.",
|
||||
"archiveSpaceDescription": "Todos los chats de este espacio se moverán al archivo para ti y otros usuarios que no sean administradores.",
|
||||
"leaveSpaceDescription": "Todos los chats dentro de este espacio se moverán al archivo. Los demás usuarios podrán ver que has abandonado el espacio.",
|
||||
"onlyAdminDescription": "Como no hay más administradores, todos los demás participantes también serán eliminados.",
|
||||
"tooltipInstructionsTitle": "¿No sabes para qué sirve?",
|
||||
"tooltipInstructionsMobileBody": "Mantenga pulsados los elementos para ver la información sobre herramientas.",
|
||||
"tooltipInstructionsBrowserBody": "Pase el ratón sobre los elementos para ver información sobre herramientas.",
|
||||
"buildTranslation": "Construye tu traducción a partir de las opciones anteriores"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
|
|||
import 'package:fluffychat/pages/settings_password/settings_password.dart';
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/guard/p_vguard.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/exchange/add_exchange_to_class.dart';
|
||||
|
|
@ -171,6 +172,28 @@ abstract class AppRoutes {
|
|||
const StudentAnalyticsPage(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'analytics',
|
||||
|
|
@ -189,6 +212,36 @@ abstract class AppRoutes {
|
|||
state,
|
||||
const ClassAnalyticsPage(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ClassAnalyticsPage(
|
||||
// when going to sub-space from within a parent space's analytics, the
|
||||
// analytics list tiles do not properly update. Adding a unique key to this page is the best fix
|
||||
// I can find at the moment
|
||||
key: UniqueKey(),
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ClassAnalyticsPage(
|
||||
// when going to sub-space from within a parent space's analytics, the
|
||||
// analytics list tiles do not properly update. Adding a unique key to this page is the best fix
|
||||
// I can find at the moment
|
||||
key: UniqueKey(),
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -68,7 +67,10 @@ class ChatPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
// #Pangea
|
||||
if (room == null || room.membership == Membership.leave) {
|
||||
// if (room == null) {
|
||||
// Pangea#
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)),
|
||||
body: Center(
|
||||
|
|
@ -654,34 +656,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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:animations/animations.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
|
|
@ -33,6 +34,25 @@ class ChatInputRow extends StatelessWidget {
|
|||
final activel2 =
|
||||
controller.pangeaController.languageController.activeL2Model();
|
||||
|
||||
String hintText() {
|
||||
if (controller.choreographer.choreoMode == ChoreoMode.it) {
|
||||
return L10n.of(context)!.buildTranslation;
|
||||
}
|
||||
return activel1 != null &&
|
||||
activel2 != null &&
|
||||
activel1.langCode != LanguageKeys.unknownLanguage &&
|
||||
activel2.langCode != LanguageKeys.unknownLanguage
|
||||
? L10n.of(context)!.writeAMessageFlag(
|
||||
activel1.languageEmoji ??
|
||||
activel1.getDisplayName(context) ??
|
||||
activel1.langCode,
|
||||
activel2.languageEmoji ??
|
||||
activel2.getDisplayName(context) ??
|
||||
activel2.langCode,
|
||||
)
|
||||
: L10n.of(context)!.writeAMessage;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ITBar(
|
||||
|
|
@ -331,21 +351,10 @@ class ChatInputRow extends StatelessWidget {
|
|||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
),
|
||||
hintText: activel1 != null &&
|
||||
activel2 != null &&
|
||||
activel1.langCode !=
|
||||
LanguageKeys.unknownLanguage &&
|
||||
activel2.langCode !=
|
||||
LanguageKeys.unknownLanguage
|
||||
? L10n.of(context)!.writeAMessageFlag(
|
||||
activel1.languageEmoji ??
|
||||
activel1.getDisplayName(context) ??
|
||||
activel1.langCode,
|
||||
activel2.languageEmoji ??
|
||||
activel2.getDisplayName(context) ??
|
||||
activel2.langCode,
|
||||
)
|
||||
: L10n.of(context)!.writeAMessage,
|
||||
// #Pangea
|
||||
// hintText: L10n.of(context)!.writeAMessage,
|
||||
hintText: hintText(),
|
||||
// Pangea#
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/stream_extension.dart';
|
||||
|
|
@ -152,6 +153,11 @@ class ChatView extends StatelessWidget {
|
|||
context: context,
|
||||
future: () => controller.room.join(),
|
||||
);
|
||||
// #Pangea
|
||||
controller.room.leaveIfFull().then(
|
||||
(full) => full ? context.go('/rooms') : null,
|
||||
);
|
||||
// Pangea#
|
||||
}
|
||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
final scrollUpBannerEventId = controller.scrollUpBannerEventId;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'package:collection/collection.dart';
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details_view.dart';
|
||||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/set_class_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/set_class_topic.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_des
|
|||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/lock_room.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
|
||||
|
|
@ -34,7 +35,10 @@ class ChatDetailsView extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.roomId!);
|
||||
if (room == null) {
|
||||
// #Pangea
|
||||
if (room == null || room.membership == Membership.leave) {
|
||||
// if (room == null) {
|
||||
// Pangea#
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context)!.oopsSomethingWentWrong),
|
||||
|
|
@ -236,8 +240,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
height: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
// #Pangea
|
||||
if (room.canSendEvent('m.room.name'))
|
||||
// if (room.canSendEvent('m.room.name'))
|
||||
if (room.isRoomAdmin)
|
||||
// #Pangea
|
||||
ClassNameButton(
|
||||
room: room,
|
||||
controller: controller,
|
||||
|
|
@ -247,6 +252,12 @@ class ChatDetailsView extends StatelessWidget {
|
|||
room: room,
|
||||
controller: controller,
|
||||
),
|
||||
// #Pangea
|
||||
RoomCapacityButton(
|
||||
room: room,
|
||||
controller: controller,
|
||||
),
|
||||
// Pangea#
|
||||
if ((room.isPangeaClass || room.isExchange) &&
|
||||
room.isRoomAdmin)
|
||||
ListTile(
|
||||
|
|
@ -435,7 +446,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
// : null,
|
||||
// ),
|
||||
// if (!room.isDirectChat)
|
||||
if (!room.isDirectChat && !room.isSpace)
|
||||
if (!room.isDirectChat &&
|
||||
!room.isSpace &&
|
||||
room.isRoomAdmin)
|
||||
// Pangea#
|
||||
ListTile(
|
||||
// #Pangea
|
||||
|
|
@ -510,7 +523,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
room: room,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (!room.isPangeaClass && !room.isDirectChat)
|
||||
if (!room.isPangeaClass &&
|
||||
!room.isDirectChat &&
|
||||
room.isRoomAdmin)
|
||||
AddToSpaceToggles(
|
||||
roomId: room.id,
|
||||
key: controller.addToSpaceKey,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/tor_stub.dart'
|
||||
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
|
||||
|
|
@ -801,7 +800,10 @@ class ChatListController extends State<ChatList>
|
|||
final selectedSpace = await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.addToSpace,
|
||||
message: L10n.of(context)!.addToSpaceDescription,
|
||||
// #Pangea
|
||||
// message: L10n.of(context)!.addToSpaceDescription,
|
||||
message: L10n.of(context)!.addSpaceToSpaceDescription,
|
||||
// Pangea#
|
||||
fullyCapitalizedForMaterial: false,
|
||||
actions: Matrix.of(context)
|
||||
.client
|
||||
|
|
@ -820,8 +822,11 @@ class ChatListController extends State<ChatList>
|
|||
.map(
|
||||
(space) => AlertDialogAction(
|
||||
key: space.id,
|
||||
label: space
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
// #Pangea
|
||||
// label: space
|
||||
// .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
label: space.nameIncludingParents(context),
|
||||
// Pangea#
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -902,6 +907,7 @@ class ChatListController extends State<ChatList>
|
|||
if (mounted) {
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
await pangeaController.subscriptionController.initialize();
|
||||
await pangeaController.myAnalytics.addEventsListener();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
|
|
|
|||
|
|
@ -138,17 +138,14 @@ class ChatListView extends StatelessWidget {
|
|||
builder: (context) {
|
||||
final allSpaces =
|
||||
client.rooms.where((room) => room.isSpace);
|
||||
// #Pangea
|
||||
// final rootSpaces = allSpaces
|
||||
// .where(
|
||||
// (space) => !allSpaces.any(
|
||||
// (parentSpace) => parentSpace.spaceChildren
|
||||
// .any((child) => child.roomId == space.id),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
final rootSpaces = allSpaces.toList();
|
||||
// Pangea#
|
||||
final rootSpaces = allSpaces
|
||||
.where(
|
||||
(space) => !allSpaces.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final destinations = getNavigationDestinations(context);
|
||||
|
||||
return SizedBox(
|
||||
|
|
@ -228,9 +225,9 @@ class ChatListView extends StatelessWidget {
|
|||
NavigationDestinationLabelBehavior.alwaysHide,
|
||||
height: 64,
|
||||
shadowColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).colorScheme.background,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
selectedIndex: controller.selectedIndex,
|
||||
onDestinationSelected:
|
||||
controller.onDestinationSelected,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_code.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
|
|
@ -69,7 +68,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.allMyAnalyticsRooms.isNotEmpty,
|
||||
enabled: matrix.client.rooms.isNotEmpty,
|
||||
value: SettingsAction.myAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -179,6 +179,12 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// Wait for room actually appears in sync
|
||||
await client.waitForRoomInSync(spaceChild.roomId, join: true);
|
||||
}
|
||||
// #Pangea
|
||||
final room = client.getRoomById(spaceChild.roomId);
|
||||
if (room != null && (await room.leaveIfFull())) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
// Pangea#
|
||||
},
|
||||
);
|
||||
if (result.error != null) return;
|
||||
|
|
@ -197,6 +203,9 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
if (await room.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
|
|
@ -271,9 +280,8 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
icon: Icons.architecture_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
|
||||
// if (room != null)
|
||||
if (room != null && room.membership != Membership.leave)
|
||||
// if (room != null)
|
||||
// Pangea#
|
||||
SheetAction(
|
||||
key: SpaceChildContextAction.leave,
|
||||
|
|
@ -329,34 +337,14 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
case SpaceChildContextAction.archive:
|
||||
widget.controller.cancelAction();
|
||||
// #Pangea
|
||||
if (room == null) return;
|
||||
// room.isSpace
|
||||
// ? await showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () async {
|
||||
// await room.archiveSpace(
|
||||
// Matrix.of(context).client,
|
||||
// );
|
||||
// widget.controller.selectedRoomIds.clear();
|
||||
// },
|
||||
// )
|
||||
// : await widget.controller.archiveAction();
|
||||
if (room.isSpace) {
|
||||
await room.archiveSpace(
|
||||
context,
|
||||
Matrix.of(context).client,
|
||||
);
|
||||
} else {
|
||||
widget.controller.toggleSelection(room.id);
|
||||
await widget.controller.archiveAction();
|
||||
}
|
||||
if (room == null || room.membership == Membership.leave) return;
|
||||
// Pangea#
|
||||
_refresh();
|
||||
break;
|
||||
case SpaceChildContextAction.addToSpace:
|
||||
widget.controller.cancelAction();
|
||||
// #Pangea
|
||||
if (room == null) return;
|
||||
if (room == null || room.membership == Membership.leave) return;
|
||||
// Pangea#
|
||||
widget.controller.toggleSelection(room.id);
|
||||
await widget.controller.addToSpace();
|
||||
|
|
@ -584,21 +572,19 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final allSpaces = client.rooms.where((room) => room.isSpace);
|
||||
if (activeSpaceId == null) {
|
||||
final rootSpaces = allSpaces
|
||||
// #Pangea
|
||||
// .where(
|
||||
// (space) =>
|
||||
// !allSpaces.any(
|
||||
// (parentSpace) => parentSpace.spaceChildren
|
||||
// .any((child) => child.roomId == space.id),
|
||||
// ) &&
|
||||
// space
|
||||
// .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||
// .toLowerCase()
|
||||
// .contains(
|
||||
// widget.controller.searchController.text.toLowerCase(),
|
||||
// ),
|
||||
//)
|
||||
// Pangea#
|
||||
.where(
|
||||
(space) =>
|
||||
!allSpaces.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
) &&
|
||||
space
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||
.toLowerCase()
|
||||
.contains(
|
||||
widget.controller.searchController.text.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return SafeArea(
|
||||
|
|
@ -614,7 +600,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: rootSpace.avatar,
|
||||
|
|
@ -949,7 +935,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
: L10n.of(context)!.enterRoom),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: isSpace
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -65,6 +66,9 @@ void onChatTap(Room room, BuildContext context) async {
|
|||
room.id,
|
||||
join: true,
|
||||
);
|
||||
if (await room.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import 'dart:typed_data';
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group_view.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chat_topic_model.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
|
@ -51,6 +53,8 @@ class NewGroupController extends State<NewGroup> {
|
|||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
|
||||
GlobalKey<ConversationBotSettingsState>();
|
||||
final GlobalKey<RoomCapacityButtonState> addCapacityKey =
|
||||
GlobalKey<RoomCapacityButtonState>();
|
||||
|
||||
ChatTopic chatTopic = ChatTopic.empty;
|
||||
|
||||
|
|
@ -145,10 +149,16 @@ class NewGroupController extends State<NewGroup> {
|
|||
visibility: sdk.Visibility.public,
|
||||
);
|
||||
}
|
||||
//#Pangea
|
||||
// #Pangea
|
||||
GoogleAnalytics.createChat(roomId);
|
||||
await addToSpaceKey.currentState!.addSpaces(roomId);
|
||||
//Pangea#
|
||||
|
||||
final capacity = addCapacityKey.currentState?.capacity;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (capacity != null && room != null) {
|
||||
room.updateRoomCapacity(capacity);
|
||||
}
|
||||
// Pangea#
|
||||
context.go('/rooms/$roomId/invite');
|
||||
} catch (e, s) {
|
||||
sdk.Logs().d('Unable to create group', e, s);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
|
|
@ -87,6 +88,9 @@ class NewGroupView extends StatelessWidget {
|
|||
// ),
|
||||
// ),
|
||||
// ),
|
||||
RoomCapacityButton(
|
||||
key: controller.addCapacityKey,
|
||||
),
|
||||
ConversationBotSettings(
|
||||
key: controller.addConversationBotKey,
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ 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/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
|
||||
|
|
@ -38,6 +38,8 @@ class NewSpaceController extends State<NewSpace> {
|
|||
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
|
||||
final GlobalKey<ClassSettingsState> classSettingsKey =
|
||||
GlobalKey<ClassSettingsState>();
|
||||
final GlobalKey<RoomCapacityButtonState> addCapacityKey =
|
||||
GlobalKey<RoomCapacityButtonState>();
|
||||
|
||||
//Pangea#
|
||||
bool loading = false;
|
||||
|
|
@ -77,7 +79,6 @@ class NewSpaceController extends State<NewSpace> {
|
|||
stateKey: '',
|
||||
content: {
|
||||
'events': {
|
||||
PangeaEventTypes.studentAnalyticsSummary: 0,
|
||||
EventTypes.spaceChild: 0,
|
||||
},
|
||||
'users_default': 0,
|
||||
|
|
@ -198,6 +199,11 @@ class NewSpaceController extends State<NewSpace> {
|
|||
}
|
||||
await Future.wait(futures);
|
||||
|
||||
final capacity = addCapacityKey.currentState?.capacity;
|
||||
final space = client.getRoomById(spaceId);
|
||||
if (capacity != null && space != null) {
|
||||
space.updateRoomCapacity(capacity);
|
||||
}
|
||||
final newChatRoomId = await Matrix.of(context).client.createGroupChat(
|
||||
enableEncryption: false,
|
||||
preset: sdk.CreateRoomPreset.publicChat,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
|
|
@ -130,6 +131,10 @@ class NewSpaceView extends StatelessWidget {
|
|||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
|
||||
RoomCapacityButton(
|
||||
key: controller.addCapacityKey,
|
||||
),
|
||||
if (controller.newClassMode)
|
||||
ClassSettings(
|
||||
key: controller.classSettingsKey,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ class IgcController {
|
|||
try {
|
||||
if (choreographer.currentText.isEmpty) return clear();
|
||||
|
||||
// the error spans are going to be reloaded, so clear the cache
|
||||
_clearCache();
|
||||
|
||||
debugPrint('getIGCTextData called with ${choreographer.currentText}');
|
||||
|
||||
debugPrint('getIGCTextData called with tokensOnly = $tokensOnly');
|
||||
|
|
@ -287,6 +290,7 @@ class IgcController {
|
|||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
_clearCache();
|
||||
// Not sure why this is here
|
||||
// MatrixState.pAnyState.closeOverlay();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ class PLocalKey {
|
|||
static const String dismissedPaywall = 'dismissedPaywall';
|
||||
static const String paywallBackoff = 'paywallBackoff';
|
||||
static const String autoPlayMessages = 'autoPlayMessages';
|
||||
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,4 +110,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,18 +6,21 @@ 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";
|
||||
static const botOptions = "pangea.bot_options";
|
||||
static const capacity = "pangea.capacity";
|
||||
|
||||
static const userAge = "pangea.user_age";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
|
|
@ -133,8 +134,9 @@ class ClassController extends BaseController {
|
|||
ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass);
|
||||
return;
|
||||
}
|
||||
|
||||
await _pangeaController.matrixState.client.joinRoom(classChunk.roomId);
|
||||
setActiveSpaceIdInChatListController(classChunk.roomId);
|
||||
|
||||
if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) ==
|
||||
null) {
|
||||
await _pangeaController.matrixState.client.waitForRoomInSync(
|
||||
|
|
@ -143,6 +145,26 @@ class ClassController extends BaseController {
|
|||
);
|
||||
}
|
||||
|
||||
// If the room is full, leave
|
||||
final room =
|
||||
_pangeaController.matrixState.client.getRoomById(classChunk.roomId);
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
if (await room.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSpaceIdInChatListController(classChunk.roomId);
|
||||
|
||||
// add the user's analytics room to this joined space
|
||||
// so their teachers can join them via the space hierarchy
|
||||
final Room? joinedSpace =
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,159 +1,313 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
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/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import '../models/constructs_analytics_model.dart';
|
||||
import '../models/student_analytics_event.dart';
|
||||
|
||||
class MyAnalyticsController {
|
||||
// controls the sending of analytics events
|
||||
class MyAnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
Timer? _updateTimer;
|
||||
final int _maxMessagesCached = 10;
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
String? get _userId => _pangeaController.matrixState.client.userID;
|
||||
// adds the listener that handles when to run automatic updates
|
||||
// to analytics - either after a certain number of messages sent/
|
||||
// received or after a certain amount of time without an update
|
||||
Future<void> addEventsListener() async {
|
||||
final Client client = _pangeaController.matrixState.client;
|
||||
|
||||
//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;
|
||||
}
|
||||
// if analytics haven't been updated in the last day, update them
|
||||
DateTime? lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
final DateTime yesterday = DateTime.now().subtract(const Duration(days: 1));
|
||||
if (lastUpdated?.isBefore(yesterday) ?? true) {
|
||||
debugPrint("analytics out-of-date, updating");
|
||||
await updateAnalytics();
|
||||
lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
}
|
||||
|
||||
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
|
||||
client.onSync.stream
|
||||
.where((SyncUpdate update) => update.rooms?.join != null)
|
||||
.listen((update) {
|
||||
updateAnalyticsTimer(update, lastUpdated);
|
||||
});
|
||||
}
|
||||
|
||||
final List<StudentAnalyticsEvent?> events = await analyticsEvents(spaces);
|
||||
// given an update from sync stream, check if the update contains
|
||||
// messages for which analytics will be saved. If so, reset the timer
|
||||
// and add the event ID to the cache of un-added event IDs
|
||||
void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) {
|
||||
for (final entry in update.rooms!.join!.entries) {
|
||||
final Room room =
|
||||
_pangeaController.matrixState.client.getRoomById(entry.key)!;
|
||||
|
||||
// get the new events in this sync that are messages
|
||||
final List<Event>? events = entry.value.timeline?.events
|
||||
?.map((event) => Event.fromMatrixEvent(event, room))
|
||||
.where((event) => eventHasAnalytics(event, lastUpdated))
|
||||
.toList();
|
||||
|
||||
// add their event IDs to the cache of un-added event IDs
|
||||
if (events == null || events.isEmpty) continue;
|
||||
for (final event in events) {
|
||||
if (event != null) {
|
||||
event.handleNewMessage(messageRecord, isEdit: isEdit);
|
||||
}
|
||||
addMessageSinceUpdate(event.eventId);
|
||||
}
|
||||
|
||||
// cancel the last timer that was set on message event and
|
||||
// reset it to fire after _minutesBeforeUpdate minutes
|
||||
_updateTimer?.cancel();
|
||||
_updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () {
|
||||
debugPrint("timer fired, updating analytics");
|
||||
updateAnalytics();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// checks if event from sync update is a message that should have analytics
|
||||
bool eventHasAnalytics(Event event, DateTime? lastUpdated) {
|
||||
return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
|
||||
event.type == EventTypes.Message &&
|
||||
event.messageType == MessageTypes.Text &&
|
||||
!(event.eventId.contains("web") &&
|
||||
!(event.eventId.contains("android")) &&
|
||||
!(event.eventId.contains("iOS")));
|
||||
}
|
||||
|
||||
// adds an event ID to the cache of un-added event IDs
|
||||
// if the event IDs isn't already added
|
||||
void addMessageSinceUpdate(String eventId) {
|
||||
final List<String> currentCache = messagesSinceUpdate;
|
||||
if (!currentCache.contains(eventId)) {
|
||||
currentCache.add(eventId);
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
currentCache,
|
||||
local: true,
|
||||
);
|
||||
}
|
||||
|
||||
// if the cached has reached if max-length, update analytics
|
||||
if (messagesSinceUpdate.length > _maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
// called before updating analytics
|
||||
void clearMessagesSinceUpdate() {
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
local: true,
|
||||
);
|
||||
}
|
||||
|
||||
// a local cache of eventIds for messages sent since the last update
|
||||
// it's possible for this cache to be invalid or deleted
|
||||
// It's a proxy measure for messages sent since last update
|
||||
List<String> get messagesSinceUpdate {
|
||||
final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
local: true,
|
||||
);
|
||||
if (locallySaved == null) {
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
local: true,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return locallySaved as List<String>;
|
||||
} catch (err) {
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
local: true,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAnalytics() async {
|
||||
// top level analytics sending function. Send analytics
|
||||
// for each type of analytics event
|
||||
// to each of the applicable analytics rooms
|
||||
clearMessagesSinceUpdate();
|
||||
|
||||
// fetch a list of all the chats that the user is studying
|
||||
// and a list of all the spaces in which the user is studying
|
||||
await setStudentChats();
|
||||
await setStudentSpaces();
|
||||
|
||||
// get all the analytics rooms that the user has
|
||||
// and create any missing analytics rooms (if the user is studying
|
||||
// in a class but doesn't have an analytics room for that class's L2)
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
analyticsRooms.addAll(await createMissingAnalyticsRooms());
|
||||
|
||||
// finally, send an analytics event for each analytics room and
|
||||
// each type of analytics event
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
for (final String type in AnalyticsEvent.analyticsEventTypes) {
|
||||
await sendAnalyticsEvent(analyticsRoom, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
) async {
|
||||
// given an analytics room for a language and a type of analytics event
|
||||
// gathers all the relevant data and sends it to the analytics room
|
||||
|
||||
// get the language code for the analytics room
|
||||
final String? langCode = analyticsRoom.madeForLang;
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "no lang code found for analytics room: ${analyticsRoom.id}",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the last time an analytics event of this type was sent to this room
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
|
||||
// each type of analytics event has a format for storing per-message data
|
||||
// for SummaryAnalytics events, this is RecentMessageRecord
|
||||
// for Construct events, this is OneConstructUse
|
||||
// analyticsContent is a list of these formatted data
|
||||
final List<dynamic> analyticsContent = [];
|
||||
|
||||
for (final Room chat in _studentChats) {
|
||||
// for each chat the student studies in, check if the langCode
|
||||
// matches the langCode of the analytics room
|
||||
final String? chatLangCode =
|
||||
_pangeaController.languageController.activeL2Code(roomID: chat.id);
|
||||
if (chatLangCode != langCode) continue;
|
||||
|
||||
// get messages the logged in user has sent in all chats
|
||||
// since the last analytics event was sent
|
||||
List<PangeaMessageEvent>? recentMsgs;
|
||||
try {
|
||||
recentMsgs = await chat.myMessageEventsInChat(
|
||||
since: lastUpdated,
|
||||
);
|
||||
} catch (err) {
|
||||
debugPrint("failed to fetch messages for chat ${chat.id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastUpdated != null) {
|
||||
recentMsgs.removeWhere(
|
||||
(msg) => msg.event.originServerTs.isBefore(lastUpdated),
|
||||
);
|
||||
}
|
||||
|
||||
// then format that data into analytics data and add the formatted
|
||||
// data to the list of analyticsContent
|
||||
analyticsContent.addAll(
|
||||
AnalyticsModel.formatAnalyticsContent(recentMsgs, type),
|
||||
);
|
||||
}
|
||||
|
||||
// send the analytics data to the analytics room
|
||||
if (analyticsContent.isEmpty) return;
|
||||
await AnalyticsEvent.sendEvent(
|
||||
analyticsRoom,
|
||||
type,
|
||||
analyticsContent,
|
||||
);
|
||||
}
|
||||
|
||||
// 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>> createMissingAnalyticsRooms() 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;
|
||||
}
|
||||
|
||||
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);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> analyticsEvents(
|
||||
List<Room> spaces,
|
||||
) async {
|
||||
final List<Future<StudentAnalyticsEvent?>> events = [];
|
||||
if (_userId != null) {
|
||||
for (final space in spaces) {
|
||||
events.add(space.getStudentAnalytics(_userId!));
|
||||
}
|
||||
}
|
||||
return Future.wait(events);
|
||||
List<Room> _studentSpaces = [];
|
||||
|
||||
Future<void> setStudentSpaces() async {
|
||||
_studentSpaces = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
}
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> allMyAnalyticsEvents() async =>
|
||||
analyticsEvents(
|
||||
await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn,
|
||||
);
|
||||
|
||||
Future<void> saveConstructsMixed(
|
||||
List<OneConstructUse> allUses,
|
||||
String langCode, {
|
||||
bool isEdit = false,
|
||||
}) async {
|
||||
List<Room> get studentSpaces {
|
||||
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);
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(saveFutures);
|
||||
} catch (err, s) {
|
||||
if (_studentSpaces.isNotEmpty) return _studentSpaces;
|
||||
setStudentSpaces();
|
||||
return _studentSpaces;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
if (!kDebugMode) rethrow;
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// used to aggregate ConstructEvents, from multiple senders (students) with the same lemma
|
||||
List<AggregateConstructUses> aggregateConstructData(
|
||||
List<ConstructEvent> constructs,
|
||||
) {
|
||||
final Map<String, List<ConstructEvent>> lemmasToConstructs = {};
|
||||
for (final construct in constructs) {
|
||||
lemmasToConstructs[construct.content.lemma] ??= [];
|
||||
lemmasToConstructs[construct.content.lemma]!.add(construct);
|
||||
}
|
||||
|
||||
final List<AggregateConstructUses> aggregatedConstructs = [];
|
||||
for (final lemmaToConstructs in lemmasToConstructs.entries) {
|
||||
final List<ConstructEvent> lemmaConstructs = lemmaToConstructs.value;
|
||||
final AggregateConstructUses aggregatedData = AggregateConstructUses(
|
||||
constructs: lemmaConstructs,
|
||||
);
|
||||
aggregatedConstructs.add(aggregatedData);
|
||||
}
|
||||
return aggregatedConstructs;
|
||||
}
|
||||
}
|
||||
|
||||
class AggregateConstructUses {
|
||||
final List<ConstructEvent> _constructs;
|
||||
|
||||
AggregateConstructUses({required List<ConstructEvent> constructs})
|
||||
: _constructs = constructs;
|
||||
|
||||
String get lemma {
|
||||
assert(
|
||||
_constructs.isNotEmpty &&
|
||||
_constructs.every(
|
||||
(construct) =>
|
||||
construct.content.lemma == _constructs.first.content.lemma,
|
||||
),
|
||||
);
|
||||
return _constructs.first.content.lemma;
|
||||
}
|
||||
|
||||
List<OneConstructUse> get uses => _constructs
|
||||
.map((construct) => construct.content.uses)
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,29 +248,11 @@ class PangeaController {
|
|||
if (!userIds.contains(BotName.byEnvironment)) {
|
||||
try {
|
||||
await space.invite(BotName.byEnvironment);
|
||||
await space.postLoad();
|
||||
await space.setPower(
|
||||
BotName.byEnvironment,
|
||||
ClassDefaultValues.powerLevelOfAdmin,
|
||||
);
|
||||
} catch (err) {
|
||||
ErrorHandler.logError(
|
||||
e: "Failed to invite pangea bot to space ${space.id}",
|
||||
);
|
||||
}
|
||||
} else if (space.getPowerLevelByUserId(BotName.byEnvironment) <
|
||||
ClassDefaultValues.powerLevelOfAdmin) {
|
||||
try {
|
||||
await space.postLoad();
|
||||
await space.setPower(
|
||||
BotName.byEnvironment,
|
||||
ClassDefaultValues.powerLevelOfAdmin,
|
||||
);
|
||||
} catch (err) {
|
||||
ErrorHandler.logError(
|
||||
e: "Failed to reset power level for pangea bot in space ${space.id}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum BarChartViewSelection {
|
||||
|
|
@ -30,4 +29,15 @@ extension BarChartViewSelectionExtension on BarChartViewSelection {
|
|||
return Icons.spellcheck_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
String get route {
|
||||
switch (this) {
|
||||
case BarChartViewSelection.messages:
|
||||
return 'messages';
|
||||
// case BarChartViewSelection.vocab:
|
||||
// return 'vocab';
|
||||
case BarChartViewSelection.grammar:
|
||||
return 'errors';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../models/chart_analytics_model.dart';
|
||||
import '../models/analytics/chart_analytics_model.dart';
|
||||
|
||||
enum TimeSpan { day, week, month, sixmonths, year }
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,13 @@ extension ClassesAndExchangesClientExtension on Client {
|
|||
.toList();
|
||||
|
||||
Future<List<Room>> get _classesAndExchangesImStudyingIn async {
|
||||
for (final Room space in rooms.where((room) => room.isSpace)) {
|
||||
final List<Room> joinedSpaces = rooms
|
||||
.where(
|
||||
(room) => room.isSpace && room.membership == Membership.join,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final Room space in joinedSpaces) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,23 +96,6 @@ extension AnalyticsClientExtension on Client {
|
|||
await Future.wait(makePublicFutures);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all the users' analytics room to all the spaces the student studies in
|
||||
// So teachers can join them via space hierarchy
|
||||
// Will not always work, as there may be spaces where students don't have permission to add chats
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/p_store.dart';
|
||||
|
||||
part "classes_and_exchanges_extension.dart";
|
||||
part "client_analytics_extension.dart";
|
||||
part "general_info_extension.dart";
|
||||
|
|
@ -31,11 +29,6 @@ extension PangeaClient on Client {
|
|||
Future<void> updateAnalyticsRoomVisibility() async =>
|
||||
await _updateAnalyticsRoomVisibility();
|
||||
|
||||
Future<void> updateMyLearningAnalyticsForAllClassesImIn([
|
||||
PLocalStore? storageService,
|
||||
]) async =>
|
||||
await _updateMyLearningAnalyticsForAllClassesImIn(storageService);
|
||||
|
||||
Future<void> addAnalyticsRoomsToAllSpaces() async =>
|
||||
await _addAnalyticsRoomsToAllSpaces();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ extension GeneralInfoClientExtension on Client {
|
|||
final List<String> adminRoomIds = [];
|
||||
for (final Room adminSpace in (await _classesAndExchangesImTeaching)) {
|
||||
adminRoomIds.add(adminSpace.id);
|
||||
final children = adminSpace.childrenAndGrandChildren;
|
||||
final List<String> adminSpaceRooms = children
|
||||
.where((e) => e.roomId != null)
|
||||
.map((e) => e.roomId!)
|
||||
.toList();
|
||||
final List<String> adminSpaceRooms = adminSpace.allSpaceChildRoomIds;
|
||||
adminRoomIds.addAll(adminSpaceRooms);
|
||||
}
|
||||
return adminRoomIds;
|
||||
|
|
|
|||
|
|
@ -20,57 +20,6 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
List<String> get _joinedChildrenRoomIds =>
|
||||
joinedChildren.map((child) => child.id).toList();
|
||||
|
||||
List<SpaceChild> get _childrenAndGrandChildren {
|
||||
if (!isSpace) return [];
|
||||
final List<SpaceChild> kids = [];
|
||||
for (final child in spaceChildren) {
|
||||
kids.add(child);
|
||||
if (child.roomId != null) {
|
||||
final Room? childRoom = client.getRoomById(child.roomId!);
|
||||
if (childRoom != null && childRoom.isSpace) {
|
||||
kids.addAll(childRoom.spaceChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
return kids.where((element) => element.roomId != null).toList();
|
||||
}
|
||||
|
||||
//this assumes that a user has been invited to all group chats in a space
|
||||
//it is a janky workaround for determining whether a spacechild is a direct chat
|
||||
//since the spaceChild object doesn't contain this info. this info is only accessible
|
||||
//when the user has joined or been invited to the room. direct chats included in
|
||||
//a space show up in spaceChildren but the user has not been invited to them.
|
||||
List<String> get _childrenAndGrandChildrenDirectChatIds {
|
||||
final List<String> nonDirectChatRoomIds = childrenAndGrandChildren
|
||||
.where((child) => child.roomId != null)
|
||||
.map((e) => client.getRoomById(e.roomId!))
|
||||
.where((r) => r != null && !r.isDirectChat)
|
||||
.map((e) => e!.id)
|
||||
.toList();
|
||||
|
||||
return childrenAndGrandChildren
|
||||
.where(
|
||||
(child) =>
|
||||
child.roomId != null &&
|
||||
!nonDirectChatRoomIds.contains(child.roomId),
|
||||
)
|
||||
.map((e) => e.roomId)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
// return childrenAndGrandChildren
|
||||
// .where((element) => element.roomId != null)
|
||||
// .where(
|
||||
// (child) {
|
||||
// final room = client.getRoomById(child.roomId!);
|
||||
// return room == null || room.isDirectChat;
|
||||
// },
|
||||
// )
|
||||
// .map((e) => e.roomId)
|
||||
// .cast<String>()
|
||||
// .toList();
|
||||
}
|
||||
|
||||
Future<List<Room>> _getChildRooms() async {
|
||||
final List<Room> children = [];
|
||||
for (final child in spaceChildren) {
|
||||
|
|
@ -145,4 +94,37 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
String _nameIncludingParents(BuildContext context) {
|
||||
String nameSoFar = getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
Room currentRoom = this;
|
||||
if (currentRoom.immediateClassParents.isEmpty) {
|
||||
return nameSoFar;
|
||||
}
|
||||
currentRoom = currentRoom.immediateClassParents.first;
|
||||
var nameToAdd =
|
||||
currentRoom.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
|
||||
nameToAdd =
|
||||
nameToAdd.length <= 10 ? nameToAdd : "${nameToAdd.substring(0, 10)}...";
|
||||
nameSoFar = '$nameToAdd > $nameSoFar';
|
||||
if (currentRoom.immediateClassParents.isEmpty) {
|
||||
return nameSoFar;
|
||||
}
|
||||
return "... > $nameSoFar";
|
||||
}
|
||||
|
||||
// gets all space children of a given space, down the
|
||||
// space tree.
|
||||
List<String> get _allSpaceChildRoomIds {
|
||||
final List<String> childIds = [];
|
||||
for (final child in spaceChildren) {
|
||||
if (child.roomId == null) continue;
|
||||
childIds.add(child.roomId!);
|
||||
final Room? room = client.getRoomById(child.roomId!);
|
||||
if (room != null && room.isSpace) {
|
||||
childIds.addAll(room._allSpaceChildRoomIds);
|
||||
}
|
||||
}
|
||||
return childIds;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,22 +62,11 @@ extension ClassAndExchangeSettingsRoomExtension on Room {
|
|||
}
|
||||
final Event? currentPower = getState(EventTypes.RoomPowerLevels);
|
||||
final Map<String, dynamic>? currentPowerContent =
|
||||
currentPower?.content as Map<String, dynamic>?;
|
||||
if (currentPowerContent == null) {
|
||||
return;
|
||||
}
|
||||
if (!(currentPowerContent.containsKey("events"))) {
|
||||
currentPowerContent["events"] = {};
|
||||
}
|
||||
final spaceChildPower =
|
||||
currentPowerContent["events"][EventTypes.spaceChild];
|
||||
final studentAnalyticsPower = currentPowerContent["events"]
|
||||
[PangeaEventTypes.studentAnalyticsSummary];
|
||||
currentPower?.content["events"] as Map<String, dynamic>?;
|
||||
final spaceChildPower = currentPowerContent?[EventTypes.spaceChild];
|
||||
|
||||
if ((spaceChildPower == null || studentAnalyticsPower == null)) {
|
||||
if (spaceChildPower == null && currentPowerContent != null) {
|
||||
currentPowerContent["events"][EventTypes.spaceChild] = 0;
|
||||
currentPowerContent["events"]
|
||||
[PangeaEventTypes.studentAnalyticsSummary] = 0;
|
||||
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension EventsRoomExtension on Room {
|
||||
Future<bool> _leaveIfFull() async {
|
||||
await postLoad();
|
||||
if (!isRoomAdmin &&
|
||||
(_capacity != null) &&
|
||||
(await _numNonAdmins) > (_capacity!)) {
|
||||
if (!isSpace) {
|
||||
markUnread(false);
|
||||
}
|
||||
await leave();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _archive() async {
|
||||
final students = (await requestParticipants())
|
||||
.where(
|
||||
|
|
@ -103,7 +117,6 @@ extension EventsRoomExtension on Room {
|
|||
required String type,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint("creating $type child for $parentEventId");
|
||||
Sentry.addBreadcrumb(Breadcrumb.fromJson(content));
|
||||
if (parentEventId.contains("web")) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -298,122 +311,153 @@ extension EventsRoomExtension 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;
|
||||
// }
|
||||
|
||||
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>> _removeEditedLemmas(
|
||||
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<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);
|
||||
|
||||
Future<void> _saveConstructUsesSameLemma(
|
||||
String lemma,
|
||||
ConstructType type,
|
||||
List<OneConstructUse> lemmaUses, {
|
||||
bool isEdit = false,
|
||||
// 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<PangeaMessageEvent>> myMessageEventsInChat({
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final ConstructEvent? localEvent = _vocabEventLocal(lemma);
|
||||
|
||||
if (isEdit) {
|
||||
lemmaUses = await removeEditedLemmas(lemmaUses);
|
||||
}
|
||||
|
||||
if (localEvent == null) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.vocab,
|
||||
lemma,
|
||||
ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(),
|
||||
final List<Event> msgEvents = await getEventsBySender(
|
||||
type: EventTypes.Message,
|
||||
sender: client.userID!,
|
||||
since: since,
|
||||
);
|
||||
final Timeline timeline = await getTimeline();
|
||||
return msgEvents
|
||||
.where((event) => (event.content['msgtype'] == MessageTypes.Text))
|
||||
.map((event) {
|
||||
return PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: true,
|
||||
);
|
||||
} else {
|
||||
localEvent.addAll(lemmaUses);
|
||||
await updateStateEvent(localEvent.event);
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
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 {
|
||||
// fetch event of a certain type by a certain sender
|
||||
// since a certain time or up to a certain amount
|
||||
Future<List<Event>> getEventsBySender({
|
||||
required String type,
|
||||
required String sender,
|
||||
DateTime? since,
|
||||
int? count,
|
||||
}) 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);
|
||||
int numberOfSearches = 0;
|
||||
final Timeline timeline = await getTimeline();
|
||||
|
||||
if (event == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception(
|
||||
"null event after creation with eventId $eventId in _createVocabEvent",
|
||||
List<Event> relevantEvents() => timeline.events
|
||||
.where((event) => event.senderId == sender && event.type == type)
|
||||
.toList();
|
||||
|
||||
bool reachedEnd() {
|
||||
if (since != null) {
|
||||
return relevantEvents().any(
|
||||
(event) => event.originServerTs.isBefore(since),
|
||||
);
|
||||
}
|
||||
if (count != null) {
|
||||
return relevantEvents().length >= count;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
while (timeline.canRequestHistory &&
|
||||
!reachedEnd() &&
|
||||
numberOfSearches < 10) {
|
||||
await timeline.requestHistory(historyCount: 100);
|
||||
numberOfSearches += 1;
|
||||
if (reachedEnd()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final List<Event> fetchedEvents = timeline.events
|
||||
.where((event) => event.senderId == sender && event.type == type)
|
||||
.toList();
|
||||
|
||||
if (since != null) {
|
||||
fetchedEvents.removeWhere(
|
||||
(event) => event.originServerTs.isBefore(since),
|
||||
);
|
||||
}
|
||||
return event;
|
||||
} catch (err, stack) {
|
||||
|
||||
final List<Event> events = [];
|
||||
for (Event event in fetchedEvents) {
|
||||
if (event.relationshipType == RelationshipTypes.edit) continue;
|
||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) {
|
||||
event = event.getDisplayEvent(timeline);
|
||||
}
|
||||
events.add(event);
|
||||
}
|
||||
|
||||
return events;
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack, data: powerLevels);
|
||||
rethrow;
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@ 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/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.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';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -21,20 +26,13 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:matrix/src/utils/space_child.dart';
|
||||
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/client_extension.dart';
|
||||
|
||||
part "children_and_parents_extension.dart";
|
||||
|
|
@ -63,47 +61,42 @@ extension PangeaRoom on Room {
|
|||
Future<void> addAnalyticsRoomsToSpace() async =>
|
||||
await _addAnalyticsRoomsToSpace();
|
||||
|
||||
Future<StudentAnalyticsEvent?> getStudentAnalytics(
|
||||
String studentId, {
|
||||
bool forcedUpdate = false,
|
||||
}) async =>
|
||||
await _getStudentAnalytics(studentId, forcedUpdate: forcedUpdate);
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> getClassAnalytics([
|
||||
List<String>? studentIds,
|
||||
]) async =>
|
||||
await _getClassAnalytics(
|
||||
studentIds,
|
||||
);
|
||||
|
||||
Future<void> updateMyLearningAnalyticsForClass([
|
||||
PLocalStore? storageService,
|
||||
]) async =>
|
||||
await _updateMyLearningAnalyticsForClass(
|
||||
storageService,
|
||||
);
|
||||
|
||||
Future<void> inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async =>
|
||||
await _inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
|
||||
|
||||
Future<void> inviteTeachersToAnalyticsRoom() async =>
|
||||
await _inviteTeachersToAnalyticsRoom();
|
||||
|
||||
// Invite teachers of 1 space to all users' analytics rooms
|
||||
Future<void> inviteSpaceTeachersToAnalyticsRooms() async =>
|
||||
await _inviteSpaceTeachersToAnalyticsRooms();
|
||||
|
||||
Future<AnalyticsEvent?> getLastAnalyticsEvent(
|
||||
String type,
|
||||
String userId,
|
||||
) async =>
|
||||
await _getLastAnalyticsEvent(type, userId);
|
||||
|
||||
Future<DateTime?> analyticsLastUpdated(String type, String userId) async {
|
||||
return await _analyticsLastUpdated(type, userId);
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> getAnalyticsEvents({
|
||||
required String type,
|
||||
required String userId,
|
||||
DateTime? since,
|
||||
}) async =>
|
||||
await _getAnalyticsEvents(type: type, since: since, userId: userId);
|
||||
|
||||
String? get madeForLang => _madeForLang;
|
||||
|
||||
bool isMadeForLang(String langCode) => _isMadeForLang(langCode);
|
||||
|
||||
// children_and_parents
|
||||
|
||||
List<Room> get joinedChildren => _joinedChildren;
|
||||
|
||||
List<String> get joinedChildrenRoomIds => _joinedChildrenRoomIds;
|
||||
|
||||
List<SpaceChild> get childrenAndGrandChildren => _childrenAndGrandChildren;
|
||||
|
||||
List<String> get childrenAndGrandChildrenDirectChatIds =>
|
||||
_childrenAndGrandChildrenDirectChatIds;
|
||||
|
||||
Future<List<Room>> getChildRooms() async => await _getChildRooms();
|
||||
|
||||
Future<void> joinSpaceChild(String roomID) async =>
|
||||
|
|
@ -116,6 +109,11 @@ extension PangeaRoom on Room {
|
|||
|
||||
List<Room> get pangeaSpaceParents => _pangeaSpaceParents;
|
||||
|
||||
String nameIncludingParents(BuildContext context) =>
|
||||
_nameIncludingParents(context);
|
||||
|
||||
List<String> get allSpaceChildRoomIds => _allSpaceChildRoomIds;
|
||||
|
||||
// class_and_exchange_settings
|
||||
|
||||
DateTime? get rulesUpdatedAt => _rulesUpdatedAt;
|
||||
|
|
@ -142,6 +140,7 @@ extension PangeaRoom on Room {
|
|||
|
||||
// events
|
||||
|
||||
Future<bool> leaveIfFull() async => await _leaveIfFull();
|
||||
Future<void> archive() async => await _archive();
|
||||
|
||||
Future<bool> archiveSpace(
|
||||
|
|
@ -203,31 +202,10 @@ extension PangeaRoom on Room {
|
|||
Future<String> updateStateEvent(Event stateEvent) =>
|
||||
_updateStateEvent(stateEvent);
|
||||
|
||||
Future<ConstructEvent> vocabEvent(
|
||||
String lemma,
|
||||
ConstructType type, [
|
||||
bool makeIfNull = false,
|
||||
]) =>
|
||||
_vocabEvent(lemma, type, makeIfNull);
|
||||
|
||||
Future<List<OneConstructUse>> removeEditedLemmas(
|
||||
List<OneConstructUse> lemmaUses,
|
||||
) async =>
|
||||
await _removeEditedLemmas(lemmaUses);
|
||||
|
||||
Future<void> saveConstructUsesSameLemma(
|
||||
String lemma,
|
||||
ConstructType type,
|
||||
List<OneConstructUse> lemmaUses, {
|
||||
bool isEdit = false,
|
||||
}) async =>
|
||||
await _saveConstructUsesSameLemma(lemma, type, lemmaUses, isEdit: isEdit);
|
||||
|
||||
Future<List<ConstructEvent>> get allConstructEvents async =>
|
||||
await _allConstructEvents;
|
||||
|
||||
// room_information
|
||||
|
||||
Future<int> get numNonAdmins async => await _numNonAdmins;
|
||||
|
||||
DateTime? get creationTime => _creationTime;
|
||||
|
||||
String? get creatorId => _creatorId;
|
||||
|
|
@ -242,7 +220,7 @@ extension PangeaRoom on Room {
|
|||
|
||||
bool get isDirectChatWithoutMe => _isDirectChatWithoutMe;
|
||||
|
||||
bool isMadeForLang(String langCode) => _isMadeForLang(langCode);
|
||||
// bool isMadeForLang(String langCode) => _isMadeForLang(langCode);
|
||||
|
||||
Future<bool> get isBotRoom async => await _isBotRoom;
|
||||
|
||||
|
|
@ -258,6 +236,11 @@ extension PangeaRoom on Room {
|
|||
|
||||
// room_settings
|
||||
|
||||
Future<void> updateRoomCapacity(int newCapacity) =>
|
||||
_updateRoomCapacity(newCapacity);
|
||||
|
||||
int? get capacity => _capacity;
|
||||
|
||||
PangeaRoomRules? get pangeaRoomRules => _pangeaRoomRules;
|
||||
|
||||
PangeaRoomRules? get firstRules => _firstRules;
|
||||
|
|
|
|||
|
|
@ -123,185 +123,6 @@ extension AnalyticsRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
StudentAnalyticsEvent? _getStudentAnalyticsLocal(String studentId) {
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "calling getStudentAnalyticsLocal on non-space room",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Event? matrixEvent = getState(
|
||||
PangeaEventTypes.studentAnalyticsSummary,
|
||||
studentId,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
if (localEvent == null) {
|
||||
await postLoad();
|
||||
localEvent = _getStudentAnalyticsLocal(studentId);
|
||||
}
|
||||
|
||||
if (studentId == client.userID && localEvent == null) {
|
||||
final Event? matrixEvent = await _createStudentAnalyticsEvent();
|
||||
if (matrixEvent != null) {
|
||||
localEvent = StudentAnalyticsEvent(event: matrixEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return localEvent;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
// invite teachers of 1 space to 1 analytics room
|
||||
Future<void> _inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async {
|
||||
if (!isSpace) {
|
||||
|
|
@ -373,4 +194,67 @@ extension AnalyticsRoomExtension on Room {
|
|||
await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AnalyticsEvent?> _getLastAnalyticsEvent(
|
||||
String type,
|
||||
String userId,
|
||||
) async {
|
||||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
sender: userId,
|
||||
count: 1,
|
||||
);
|
||||
if (events.isEmpty) return null;
|
||||
final Event event = events.first;
|
||||
AnalyticsEvent? analyticsEvent;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
analyticsEvent = SummaryAnalyticsEvent(event: event);
|
||||
case PangeaEventTypes.construct:
|
||||
analyticsEvent = ConstructAnalyticsEvent(event: event);
|
||||
}
|
||||
return analyticsEvent;
|
||||
}
|
||||
|
||||
Future<DateTime?> _analyticsLastUpdated(String type, String userId) async {
|
||||
final lastEvent = await _getLastAnalyticsEvent(type, userId);
|
||||
return lastEvent?.event.originServerTs;
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> _getAnalyticsEvents({
|
||||
required String type,
|
||||
required String userId,
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
sender: userId,
|
||||
since: since,
|
||||
);
|
||||
final List<AnalyticsEvent> analyticsEvents = [];
|
||||
for (final Event event in events) {
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
analyticsEvents.add(SummaryAnalyticsEvent(event: event));
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
analyticsEvents.add(ConstructAnalyticsEvent(event: event));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return analyticsEvents;
|
||||
}
|
||||
|
||||
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 ||
|
||||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension RoomInformationRoomExtension on Room {
|
||||
Future<int> get _numNonAdmins async {
|
||||
return (await requestParticipants())
|
||||
.where(
|
||||
(e) =>
|
||||
e.powerLevel < ClassDefaultValues.powerLevelOfAdmin &&
|
||||
e.id != BotName.byEnvironment,
|
||||
)
|
||||
.toList()
|
||||
.length;
|
||||
}
|
||||
|
||||
DateTime? get _creationTime =>
|
||||
getState(EventTypes.RoomCreate)?.originServerTs;
|
||||
|
||||
|
|
@ -34,11 +45,11 @@ extension RoomInformationRoomExtension on Room {
|
|||
bool get _isDirectChatWithoutMe =>
|
||||
isDirectChat && !getParticipants().any((e) => e.id == client.userID);
|
||||
|
||||
bool _isMadeForLang(String langCode) {
|
||||
final creationContent = getState(EventTypes.RoomCreate)?.content;
|
||||
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
|
||||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
}
|
||||
// bool _isMadeForLang(String langCode) {
|
||||
// final creationContent = getState(EventTypes.RoomCreate)?.content;
|
||||
// return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
|
||||
// creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
// }
|
||||
|
||||
Future<bool> get _isBotRoom async {
|
||||
final List<User> participants = await requestParticipants();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension RoomSettingsRoomExtension on Room {
|
||||
Future<void> _updateRoomCapacity(int newCapacity) =>
|
||||
client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.capacity,
|
||||
'',
|
||||
{'capacity': newCapacity},
|
||||
);
|
||||
|
||||
int? get _capacity {
|
||||
final t = getState(PangeaEventTypes.capacity)?.content['capacity'];
|
||||
return t is int ? t : null;
|
||||
}
|
||||
|
||||
PangeaRoomRules? get _pangeaRoomRules {
|
||||
try {
|
||||
final Map<String, dynamic>? content = pangeaRoomRulesStateEvent?.content;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
59
lib/pangea/models/analytics/analytics_event.dart
Normal file
59
lib/pangea/models/analytics/analytics_event.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
// superclass for all analytics events
|
||||
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!;
|
||||
}
|
||||
|
||||
static List<String> analyticsEventTypes = [
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
PangeaEventTypes.construct,
|
||||
];
|
||||
|
||||
static Future<String?> sendEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
List<dynamic> analyticsContent,
|
||||
) async {
|
||||
String? eventId;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<RecentMessageRecord>(),
|
||||
);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
eventId = await ConstructAnalyticsEvent.sendConstructsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<OneConstructUse>(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
19
lib/pangea/models/analytics/analytics_model.dart
Normal file
19
lib/pangea/models/analytics/analytics_model.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
|
||||
abstract class AnalyticsModel {
|
||||
static List<dynamic> formatAnalyticsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
String type,
|
||||
) {
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
|
||||
class TimeSeriesTotals {
|
||||
int ta;
|
||||
|
|
@ -137,4 +137,13 @@ class ChartAnalyticsModel {
|
|||
}
|
||||
timeSeries = intervals.values.toList().reversed.toList();
|
||||
}
|
||||
|
||||
DateTime? get lastMessageTime {
|
||||
if (msgs.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return msgs.map((msg) => msg.time).reduce(
|
||||
(compare, recent) => compare.isAfter(recent) ? compare : recent,
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/pangea/models/analytics/constructs_event.dart
Normal file
36
lib/pangea/models/analytics/constructs_event.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/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;
|
||||
}
|
||||
|
||||
static Future<String?> sendConstructsEvent(
|
||||
Room analyticsRoom,
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +1,105 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../enum/construct_type_enum.dart';
|
||||
|
||||
class ConstructUses {
|
||||
String lemma;
|
||||
ConstructType type;
|
||||
import '../../enum/construct_type_enum.dart';
|
||||
|
||||
class ConstructAnalyticsModel extends AnalyticsModel {
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
//PTODO - how to incorporate semantic similarity score into this?
|
||||
|
||||
//PTODO - add variables for saving requests for
|
||||
// 1) definitions
|
||||
// 2) translations
|
||||
// 3) examples??? (gpt suggested)
|
||||
|
||||
ConstructUses({
|
||||
required this.lemma,
|
||||
required this.type,
|
||||
ConstructAnalyticsModel({
|
||||
this.uses = const [],
|
||||
});
|
||||
|
||||
factory ConstructUses.fromJson(Map<String, dynamic> json) {
|
||||
// try {
|
||||
debugger(
|
||||
when:
|
||||
kDebugMode && (json['uses'] == null || json[ModelKey.lemma] == null),
|
||||
static const _usesKey = "uses";
|
||||
|
||||
factory ConstructAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
if (json[_usesKey] is List) {
|
||||
// This is the new format
|
||||
uses.addAll(
|
||||
json[_usesKey]
|
||||
.map((use) => OneConstructUse.fromJson(use))
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
// This is the old format. No data on production should be
|
||||
// structured this way, but it's useful for testing.
|
||||
try {
|
||||
final useValues = (json[_usesKey] as Map<String, dynamic>).values;
|
||||
for (final useValue in useValues) {
|
||||
final lemma = useValue['lemma'];
|
||||
final lemmaUses = useValue[_usesKey];
|
||||
for (final useData in lemmaUses) {
|
||||
final use = OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
chatId: useData["chatId"],
|
||||
timeStamp: DateTime.parse(useData["timeStamp"]),
|
||||
lemma: lemma,
|
||||
form: useData["form"],
|
||||
msgId: useData["msgId"],
|
||||
constructType: ConstructType.grammar,
|
||||
);
|
||||
uses.add(use);
|
||||
}
|
||||
}
|
||||
} catch (err, s) {
|
||||
debugPrint("Error parsing ConstructAnalyticsModel");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
m: "Error parsing ConstructAnalyticsModel",
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
return ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
return ConstructUses(
|
||||
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']),
|
||||
);
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// rethrow;
|
||||
// }
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
ModelKey.lemma: lemma,
|
||||
'uses': uses.map((use) => use.toJson()).toList(),
|
||||
'type': type.string,
|
||||
_usesKey: uses.map((use) => use.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
void addUsesByUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
if (use.lemma != lemma) {
|
||||
throw Exception('lemma mismatch');
|
||||
}
|
||||
uses.add(use);
|
||||
static List<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
for (final msg in filtered) {
|
||||
if (msg.originalSent?.choreo == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
|
||||
final List<PangeaToken>? tokens = msg.originalSent?.tokens;
|
||||
if (tokens == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toVocabUse(
|
||||
tokens,
|
||||
msg.room.id,
|
||||
msg.eventId,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +189,7 @@ class OneConstructUse {
|
|||
String chatId;
|
||||
String? msgId;
|
||||
DateTime timeStamp;
|
||||
String? id;
|
||||
|
||||
OneConstructUse({
|
||||
required this.useType,
|
||||
|
|
@ -162,6 +199,7 @@ class OneConstructUse {
|
|||
required this.form,
|
||||
required this.msgId,
|
||||
required this.constructType,
|
||||
this.id,
|
||||
});
|
||||
|
||||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -176,10 +214,11 @@ class OneConstructUse {
|
|||
constructType: json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
: null,
|
||||
id: json['id'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson([bool condensed = true]) {
|
||||
Map<String, dynamic> toJson([bool condensed = false]) {
|
||||
final Map<String, String?> data = {
|
||||
'useType': useType.string,
|
||||
'chatId': chatId,
|
||||
|
|
@ -191,6 +230,7 @@ class OneConstructUse {
|
|||
if (!condensed && constructType != null) {
|
||||
data['constructType'] = constructType!.string;
|
||||
}
|
||||
if (id != null) data['id'] = id;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -205,3 +245,15 @@ class OneConstructUse {
|
|||
return room.getEventById(msgId!);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructType constructType;
|
||||
final String lemma;
|
||||
|
||||
ConstructUses({
|
||||
required this.uses,
|
||||
required this.constructType,
|
||||
required this.lemma,
|
||||
});
|
||||
}
|
||||
35
lib/pangea/models/analytics/summary_analytics_event.dart
Normal file
35
lib/pangea/models/analytics/summary_analytics_event.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/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;
|
||||
}
|
||||
|
||||
static Future<String?> sendSummaryAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,64 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../enum/use_type.dart';
|
||||
class SummaryAnalyticsModel extends AnalyticsModel {
|
||||
late List<RecentMessageRecord> _messages;
|
||||
|
||||
SummaryAnalyticsModel({
|
||||
required List<RecentMessageRecord> messages,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
static List<RecentMessageRecord> formatSummaryContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<RecentMessageRecord> records = filtered
|
||||
.map(
|
||||
(msg) => RecentMessageRecord(
|
||||
eventId: msg.eventId,
|
||||
chatId: msg.room.id,
|
||||
useType: msg.useType,
|
||||
time: msg.originServerTs,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
class RecentMessageRecord {
|
||||
String eventId;
|
||||
|
|
@ -55,62 +109,3 @@ class RecentMessageRecord {
|
|||
static const _typeOfUseKey = "typ";
|
||||
static const _timeKey = "t";
|
||||
}
|
||||
|
||||
class StudentAnalyticsSummary {
|
||||
late List<RecentMessageRecord> _messages;
|
||||
DateTime lastUpdated;
|
||||
|
||||
StudentAnalyticsSummary({
|
||||
required List<RecentMessageRecord> messages,
|
||||
required this.lastUpdated,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
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 {
|
||||
_messages.add(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeEdittedMessages(Client client, List<String> removeEventIds) {
|
||||
_messages.removeWhere(
|
||||
(element) => removeEventIds.contains(element.eventId),
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
factory StudentAnalyticsSummary.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 StudentAnalyticsSummary(
|
||||
messages: savedMessages,
|
||||
lastUpdated: DateTime.parse(json[_lastUpdatedKey]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
// import 'dart:convert';
|
||||
|
||||
// class UserTimeSeriesInterval {
|
||||
// String? userId;
|
||||
// int? taTotal;
|
||||
// int? gaTotal;
|
||||
// int? waTotal;
|
||||
|
||||
// UserTimeSeriesInterval({
|
||||
// required this.userId,
|
||||
// required this.taTotal,
|
||||
// required this.gaTotal,
|
||||
// required this.waTotal,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toJson() =>
|
||||
// {"usr": userId, "ta": taTotal, "ga": gaTotal, "wa": waTotal};
|
||||
|
||||
// factory UserTimeSeriesInterval.fromJson(json) => UserTimeSeriesInterval(
|
||||
// userId: json["usr"],
|
||||
// taTotal: json["ta"],
|
||||
// gaTotal: json["ga"],
|
||||
// waTotal: json["wa"],
|
||||
// );
|
||||
// }
|
||||
|
||||
// class TimeSeriesInterval {
|
||||
// DateTime start;
|
||||
// DateTime end;
|
||||
// List<UserTimeSeriesInterval> users;
|
||||
|
||||
// TimeSeriesInterval({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.users,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// "strt": start,
|
||||
// "end": end,
|
||||
// "usrs": jsonEncode(users.map((e) => e.toJson()).toList())
|
||||
// };
|
||||
|
||||
// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
|
||||
// start: json["strt"],
|
||||
// end: json["end"],
|
||||
// users: ((jsonDecode(json["usrs"]) as Iterable)
|
||||
// .map((e) => UserTimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<UserTimeSeriesInterval>()),
|
||||
// );
|
||||
// }
|
||||
|
||||
// class RoomAnalyticsSummary {
|
||||
// List<TimeSeriesInterval> monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> hourlyTotalsForLast24Hours;
|
||||
|
||||
// DateTime? updatedAt;
|
||||
|
||||
// RoomAnalyticsSummary({
|
||||
// required this.monthlyTotalsForAllTime,
|
||||
// required this.dailyTotalsForLast30Days,
|
||||
// required this.hourlyTotalsForLast24Hours,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// "mnths":
|
||||
// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()),
|
||||
// "dys": jsonEncode(
|
||||
// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()),
|
||||
// "hrs": jsonEncode(
|
||||
// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()),
|
||||
// };
|
||||
|
||||
// factory RoomAnalyticsSummary.fromJson(json) => RoomAnalyticsSummary(
|
||||
// monthlyTotalsForAllTime: (jsonDecode(json["mnths"]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// dailyTotalsForLast30Days: (jsonDecode(json["dys"]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// hourlyTotalsForLast24Hours: (jsonDecode(json["hrs"]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// class UserDirectChatAnalyticsSummary {
|
||||
// // directChatRoomIds and analytics for those rooms
|
||||
// // updated by user;
|
||||
// Map<String, RoomAnalyticsSummary>? directChatSummaries;
|
||||
|
||||
// Map<String, dynamic> toJson() => {};
|
||||
// }
|
||||
|
||||
// // maybe search how to do date ranges in dart
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
// import 'dart:convert';
|
||||
|
||||
// class ChatTimeSeriesInterval {
|
||||
// String? chatId;
|
||||
// int? taTotal;
|
||||
// int? gaTotal;
|
||||
// int? waTotal;
|
||||
|
||||
// ChatTimeSeriesInterval({
|
||||
// required this.chatId,
|
||||
// required this.taTotal,
|
||||
// required this.gaTotal,
|
||||
// required this.waTotal,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toJson() =>
|
||||
// {"id": chatId, "ta": taTotal, "ga": gaTotal, "wa": waTotal};
|
||||
|
||||
// factory ChatTimeSeriesInterval.fromJson(json) => ChatTimeSeriesInterval(
|
||||
// chatId: json["id"],
|
||||
// taTotal: json["ta"],
|
||||
// gaTotal: json["ga"],
|
||||
// waTotal: json["wa"],
|
||||
// );
|
||||
// }
|
||||
|
||||
// class TimeSeriesInterval {
|
||||
// DateTime start;
|
||||
// DateTime end;
|
||||
// List<ChatTimeSeriesInterval> chats;
|
||||
|
||||
// TimeSeriesInterval({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.chats,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// "strt": start,
|
||||
// "end": end,
|
||||
// "usrs": jsonEncode(chats.map((e) => e.toJson()).toList())
|
||||
// };
|
||||
|
||||
// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
|
||||
// start: DateTime(json["strt"]),
|
||||
// end: DateTime(json["end"]),
|
||||
// chats: ((jsonDecode(json["usrs"]) as Iterable)
|
||||
// .map((e) => ChatTimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<ChatTimeSeriesInterval>()),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // class RecentMessageRecord {
|
||||
// // String eventId;
|
||||
// // String typeOfUse;
|
||||
// // String time;
|
||||
// // }
|
||||
|
||||
// class StudentAnalyticsSummary {
|
||||
// /// event statekey = studentId
|
||||
// // String studentId;
|
||||
|
||||
// List<TimeSeriesInterval> monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> hourlyTotalsForLast24Hours;
|
||||
|
||||
// // List<RecentMessageRecord> messages;
|
||||
|
||||
// DateTime lastLogin;
|
||||
// DateTime lastMessage;
|
||||
|
||||
// DateTime lastUpdated;
|
||||
|
||||
// StudentAnalyticsSummary({
|
||||
// // required this.studentId,
|
||||
// required this.monthlyTotalsForAllTime,
|
||||
// required this.dailyTotalsForLast30Days,
|
||||
// required this.hourlyTotalsForLast24Hours,
|
||||
// required this.lastLogin,
|
||||
// required this.lastMessage,
|
||||
// required this.lastUpdated,
|
||||
// });
|
||||
|
||||
// // static const _studentIdKey = 'usr';
|
||||
// static const _monthKey = "mnths";
|
||||
// static const _dayKey = "dys";
|
||||
// static const _hoursKey = "hrs";
|
||||
// static const _lastLoginKey = "lgn";
|
||||
// static const _lastMessageKey = "msg";
|
||||
// static const _lastUpdated = "lupt";
|
||||
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// _monthKey:
|
||||
// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()),
|
||||
// _dayKey: jsonEncode(
|
||||
// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()),
|
||||
// _hoursKey: jsonEncode(
|
||||
// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()),
|
||||
// // _studentIdKey: studentId,
|
||||
// _lastLoginKey: lastLogin.toIso8601String(),
|
||||
// _lastMessageKey: lastMessage.toIso8601String(),
|
||||
// _lastUpdated: lastUpdated.toIso8601String()
|
||||
// };
|
||||
|
||||
// factory StudentAnalyticsSummary.fromJson(json) => StudentAnalyticsSummary(
|
||||
// // studentId: json[_studentIdKey],
|
||||
// monthlyTotalsForAllTime: (jsonDecode(json[_monthKey]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// dailyTotalsForLast30Days: (jsonDecode(json[_dayKey]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// hourlyTotalsForLast24Hours: (jsonDecode(json[_hoursKey]) as Iterable)
|
||||
// .map((e) => TimeSeriesInterval.fromJson(e))
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>(),
|
||||
// lastLogin: DateTime(json[_lastLoginKey]),
|
||||
// lastUpdated: DateTime(json[_lastLoginKey]),
|
||||
// lastMessage: DateTime(json[_lastMessageKey]),
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// import 'dart:convert';
|
||||
|
||||
// class BaseDataModel {
|
||||
// late int spanTotal;
|
||||
// late int spanIT;
|
||||
// late int spanIGC;
|
||||
// late int spanDirect;
|
||||
|
||||
// BaseDataModel(Map<String, dynamic> json) {
|
||||
// fromJson(json);
|
||||
// }
|
||||
|
||||
// fromJson(Map<String, dynamic> json) {
|
||||
// spanTotal = json["total"];
|
||||
// spanIT = json["it"];
|
||||
// spanIGC = json["igc"];
|
||||
// spanDirect = json["direct"];
|
||||
// }
|
||||
// }
|
||||
|
||||
// class TimeSeriesInterval extends BaseDataModel {
|
||||
// //note: always in UTC
|
||||
// late DateTime start;
|
||||
// late DateTime end;
|
||||
|
||||
// TimeSeriesInterval(Map<String, dynamic> json) : super(json) {
|
||||
// fromJsonTimeSeriesInterval(json);
|
||||
// }
|
||||
|
||||
// fromJsonTimeSeriesInterval(Map<String, dynamic> json) {
|
||||
// start = DateTime.parse(json["start"]);
|
||||
// end = DateTime.parse(json["end"]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// class chartAnalytics extends BaseDataModel {
|
||||
// late String id;
|
||||
// late int allTotal;
|
||||
// late int allIT;
|
||||
// late int allIGC;
|
||||
// late int allDirect;
|
||||
// late String timeSpan;
|
||||
// late DateTime fetchedAt;
|
||||
// late List<String>? chatIds;
|
||||
// late List<String>? userIds;
|
||||
// late List<String>? classIds;
|
||||
// late List<TimeSeriesInterval> timeSeries;
|
||||
|
||||
// chartAnalytics(Map<String, dynamic> json) : super(json) {
|
||||
// fromJsonchartAnalytics(json);
|
||||
// fetchedAt = DateTime.now();
|
||||
// }
|
||||
|
||||
// fromJsonchartAnalytics(Map<String, dynamic> json) {
|
||||
// id = json["id"];
|
||||
// timeSpan = json["timespan"];
|
||||
// allTotal = json["alltime"]["total"];
|
||||
// allIT = json["alltime"]["it"];
|
||||
// allIGC = json["alltime"]["igc"];
|
||||
// allDirect = json["alltime"]["direct"];
|
||||
// timeSeries = (json["timeseries"] as Iterable<dynamic>)
|
||||
// .map(
|
||||
// (timeSeriesJsonEntry) => TimeSeriesInterval(timeSeriesJsonEntry),
|
||||
// )
|
||||
// .toList()
|
||||
// .cast<TimeSeriesInterval>();
|
||||
// chatIds = json["chats"] != null && json["chats"] != []
|
||||
// ? (json["chats"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// userIds = json["users"] != null && json["userIds"] != []
|
||||
// ? (json["users"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// classIds = json["classes"] != null && json["classes"] != []
|
||||
// ? (json["classes"] as List<dynamic>).cast<String>()
|
||||
// : null;
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
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';
|
||||
import 'it_step.dart';
|
||||
import 'lemma.dart';
|
||||
|
||||
|
|
@ -126,6 +127,7 @@ class ChoreoRecord {
|
|||
List<PangeaToken> tokens,
|
||||
String chatId,
|
||||
String msgId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final DateTime now = DateTime.now();
|
||||
|
|
@ -140,7 +142,7 @@ class ChoreoRecord {
|
|||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: chatId,
|
||||
timeStamp: now,
|
||||
timeStamp: timestamp,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: msgId,
|
||||
|
|
@ -210,9 +212,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 +227,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}",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
class ClassAnalyticsModel {
|
||||
ClassAnalyticsModel();
|
||||
late final Null classId;
|
||||
late final List<String> userIds;
|
||||
late final List<Analytics> analytics;
|
||||
get tableView {}
|
||||
ClassAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
classId = null;
|
||||
userIds = List.castFrom<dynamic, String>(json['user_ids']);
|
||||
analytics =
|
||||
List.from(json['analytics']).map((e) => Analytics.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['class_id'] = classId;
|
||||
data['user_ids'] = userIds;
|
||||
data['analytics'] = analytics.map((e) => e.toJson()).toList();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Analytics {
|
||||
Analytics({
|
||||
required this.title,
|
||||
required this.section,
|
||||
});
|
||||
late final String title;
|
||||
late final List<Section> section;
|
||||
|
||||
Analytics.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
section =
|
||||
List.from(json['section']).map((e) => Section.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['title'] = title;
|
||||
data['section'] = section.map((e) => e.toJson()).toList();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Section {
|
||||
Section({
|
||||
required this.title,
|
||||
required this.classTotal,
|
||||
required this.data,
|
||||
});
|
||||
late final String title;
|
||||
late final String classTotal;
|
||||
late final List<Data> data;
|
||||
|
||||
Section.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
classTotal = json['class_total'];
|
||||
data = List.from(json['data']).map((e) => Data.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['title'] = title;
|
||||
data['class_total'] = classTotal;
|
||||
(data['data'] as List).map((item) => Data.fromJson(item)).toList();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Data {
|
||||
Data();
|
||||
set value(String val) => _value = val;
|
||||
String get value {
|
||||
if (value_type == 'date') {
|
||||
return DateFormat('yyyy/M/dd hh:mm a')
|
||||
.format(DateTime.parse(_value).toLocal())
|
||||
.toString();
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
|
||||
late final String userId;
|
||||
late final String _value;
|
||||
late final String value_type;
|
||||
Data.fromJson(Map<String, dynamic> json) {
|
||||
userId = json['user_id'];
|
||||
_value = json['value'];
|
||||
value_type = json['value_type'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
data['user_id'] = userId;
|
||||
data['value'] = _value;
|
||||
data['value_type'] = value_type;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/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';
|
||||
|
||||
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;
|
||||
|
||||
_messagesToSave = [];
|
||||
}
|
||||
|
||||
Room get classRoom => _event.room;
|
||||
|
||||
Event get event => _event;
|
||||
|
||||
StudentAnalyticsSummary get content {
|
||||
_contentCache ??= StudentAnalyticsSummary.fromJson(event.content);
|
||||
return _contentCache!;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
await removeEdittedMessages(message);
|
||||
}
|
||||
_addMessage(message);
|
||||
|
||||
if (DateTime.now().difference(content.lastUpdated).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToUpdateMyAnalytics) {
|
||||
_updateStudentAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
_messagesToSave.addAll(messages);
|
||||
_updateStudentAnalytics();
|
||||
}
|
||||
|
||||
Future<void> _updateStudentAnalytics() async {
|
||||
content.lastUpdated = DateTime.now();
|
||||
content.addAll(_messagesToSave);
|
||||
debugPrint("updating student analytics");
|
||||
_clearMessages();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_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<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
// import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics_model_older.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
// import '../constants/pangea_event_types.dart';
|
||||
|
||||
// class StudentAnalyticsEvent {
|
||||
// late Event _event;
|
||||
// StudentAnalyticsSummary? _contentCache;
|
||||
|
||||
// StudentAnalyticsEvent({required Event event}) {
|
||||
// if (event.type != PangeaEventTypes.studentAnalyticsSummary) {
|
||||
// throw Exception(
|
||||
// "${event.type} should not be used to make a StudentAnalyticsEvent",
|
||||
// );
|
||||
// }
|
||||
// _event = event;
|
||||
// }
|
||||
|
||||
// Event get event => _event;
|
||||
|
||||
// StudentAnalyticsSummary get _content {
|
||||
// _contentCache ??= event.getPangeaContent<StudentAnalyticsSummary>();
|
||||
// return _contentCache!;
|
||||
// }
|
||||
|
||||
// List<TimeSeriesInterval> get monthly => _content.monthlyTotalsForAllTime;
|
||||
// List<TimeSeriesInterval> get daily => _content.dailyTotalsForLast30Days;
|
||||
// List<TimeSeriesInterval> get hourly => _content.hourlyTotalsForLast24Hours;
|
||||
|
||||
// // updateLocal
|
||||
// // updateServer
|
||||
// handleNewMessage() {}
|
||||
|
||||
// /// if monthly.isNotEmpty && last.end.month < now.month
|
||||
// /// push empty intervals until last.end.month >= now.month
|
||||
// /// if daily.isEmpty
|
||||
// /// push empty intervals until last.end.day >= now.day
|
||||
// /// else if daily.where(e => e.month < now.month)
|
||||
// /// sum and add to monthly
|
||||
// ///
|
||||
// /// if hourly.isEmpty || last.end.hour < now.hour
|
||||
// /// push empty intervals until last.end.hour >= now.hour
|
||||
// /// increment hourly
|
||||
|
||||
// updateLocal() {}
|
||||
|
||||
// // if server copy is older than x, push local version
|
||||
// // get new server copy, local version = server copy
|
||||
// updateServer() {}
|
||||
// }
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -7,122 +11,148 @@ import 'package:matrix/matrix.dart';
|
|||
import '../../../../utils/date_time_extension.dart';
|
||||
import '../../../widgets/avatar.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../models/chart_analytics_model.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
import 'base_analytics.dart';
|
||||
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,
|
||||
this.showSpaceAnalytics = true,
|
||||
required this.pangeaController,
|
||||
this.controller,
|
||||
// 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 bool showSpaceAnalytics;
|
||||
|
||||
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 BaseAnalyticsController? controller;
|
||||
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?.lastMessageTime?.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: ListSummaryAnalytics(
|
||||
chartAnalytics: tileData,
|
||||
),
|
||||
selected: widget.isSelected,
|
||||
onTap: () {
|
||||
if (widget.controller?.widget.selectedView == null) {
|
||||
widget.onTap(widget.selected);
|
||||
return;
|
||||
}
|
||||
if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
|
||||
final String selectedView =
|
||||
widget.controller!.widget.selectedView!.route;
|
||||
context.go('/rooms/analytics/${room!.id}/$selectedView');
|
||||
return;
|
||||
}
|
||||
widget.onTap(widget.selected);
|
||||
},
|
||||
trailing: (room?.isSpace ?? false) &&
|
||||
widget.selected.type != AnalyticsEntryType.privateChats &&
|
||||
widget.allowNavigateOnSelect
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.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';
|
||||
|
|
@ -12,12 +14,12 @@ import '../../../widgets/matrix.dart';
|
|||
import '../../controllers/pangea_controller.dart';
|
||||
import '../../enum/bar_chart_view_enum.dart';
|
||||
import '../../enum/time_span.dart';
|
||||
import '../../models/chart_analytics_model.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
|
||||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final List<TabData> tabs;
|
||||
final Future Function(BuildContext) refreshData;
|
||||
final BarChartViewSelection? selectedView;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
|
|
@ -27,9 +29,9 @@ class BaseAnalyticsPage extends StatefulWidget {
|
|||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabs,
|
||||
required this.refreshData,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
this.selectedView,
|
||||
this.myAnalyticsController,
|
||||
});
|
||||
|
||||
|
|
@ -39,156 +41,126 @@ class BaseAnalyticsPage extends StatefulWidget {
|
|||
|
||||
class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
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;
|
||||
Room? get activeSpace {
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.space) {
|
||||
return Matrix.of(context).client.getRoomById(widget.defaultSelected.id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (analyticsSelected.type == AnalyticsEntryType.privateChats) {
|
||||
return pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: analyticsSelected.id,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.student) {
|
||||
runFirstRefresh();
|
||||
}
|
||||
setChartData();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) {
|
||||
// when a user is a parent space's analytics and clicks on a subspace
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.defaultSelected.id != widget.defaultSelected.id) {
|
||||
setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> runFirstRefresh() async {
|
||||
final analyticsRooms =
|
||||
pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
final List<AnalyticsEvent> analyticsEvent = [];
|
||||
for (final analyticsRoom in analyticsRooms) {
|
||||
final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
Matrix.of(context).client.userID!,
|
||||
);
|
||||
final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
PangeaEventTypes.construct,
|
||||
Matrix.of(context).client.userID!,
|
||||
);
|
||||
if (lastSummaryEvent != null) {
|
||||
analyticsEvent.add(lastSummaryEvent);
|
||||
}
|
||||
if (lastConstructEvent != null) {
|
||||
analyticsEvent.add(lastConstructEvent);
|
||||
}
|
||||
}
|
||||
|
||||
String? chatId = analyticsSelected.type == AnalyticsEntryType.room
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
if (analyticsEvent.isNotEmpty) return;
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
String? studentId = analyticsSelected.type == AnalyticsEntryType.student
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
Future<void> onRefresh() async {
|
||||
// postframe callback to avoid calling this function during build
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
debugPrint("updating analytics");
|
||||
await pangeaController.myAnalytics.updateAnalytics();
|
||||
await setChartData(forceUpdate: true);
|
||||
refreshStream.add(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
Future<ChartAnalyticsModel> fetchChartData(
|
||||
AnalyticsSelected? params, {
|
||||
forceUpdate = false,
|
||||
}) async {
|
||||
final ChartAnalyticsModel 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;
|
||||
|
||||
void navigate() {
|
||||
if (currentLemma != null) {
|
||||
setCurrentLemma(null);
|
||||
} else if (selectedView != null) {
|
||||
setSelectedView(null);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
removeIT: true,
|
||||
);
|
||||
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
Future.delayed(Duration.zero, () => setState(() {}));
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setSelectedView(BarChartViewSelection? view) {
|
||||
currentLemma = null;
|
||||
selectedView = view;
|
||||
if (!enableSelection(selected)) {
|
||||
toggleSelection(selected!);
|
||||
}
|
||||
setState(() {});
|
||||
await setChartData();
|
||||
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
|
||||
|
|
@ -221,6 +193,19 @@ class TabItem {
|
|||
|
||||
enum AnalyticsEntryType { student, room, space, privateChats }
|
||||
|
||||
extension AnalyticsEntryTypeExtension on AnalyticsEntryType {
|
||||
String get route {
|
||||
switch (this) {
|
||||
case AnalyticsEntryType.student:
|
||||
return 'mylearning';
|
||||
case AnalyticsEntryType.space:
|
||||
return 'analytics';
|
||||
default:
|
||||
throw Exception('No route for $this');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsSelected {
|
||||
String id;
|
||||
AnalyticsEntryType type;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class BaseAnalyticsView extends StatelessWidget {
|
||||
const BaseAnalyticsView({
|
||||
|
|
@ -22,17 +23,14 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
final BaseAnalyticsController controller;
|
||||
|
||||
Widget chartView(BuildContext context) {
|
||||
if (controller.selectedView == null) {
|
||||
if (controller.widget.selectedView == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
switch (controller.selectedView!) {
|
||||
switch (controller.widget.selectedView!) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData(
|
||||
context,
|
||||
controller.selected,
|
||||
),
|
||||
chartAnalytics: controller.chartData,
|
||||
);
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
|
|
@ -41,6 +39,7 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
selected: controller.selected,
|
||||
controller: controller,
|
||||
pangeaController: controller.pangeaController,
|
||||
refreshStream: controller.refreshStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -62,55 +61,68 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
text: controller.widget.pageTitle,
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => controller.selectedView != null
|
||||
? controller.setSelectedView(null)
|
||||
: null,
|
||||
..onTap = () {
|
||||
final String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
if (controller.selectedView != null)
|
||||
if (controller.activeSpace != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.selectedView != null)
|
||||
if (controller.activeSpace != null)
|
||||
TextSpan(
|
||||
text: controller.activeSpace!.getLocalizedDisplayname(),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
text: controller.selectedView!.string(context),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => controller.currentLemma != null
|
||||
? controller.setCurrentLemma(null)
|
||||
: null,
|
||||
..onTap = () {
|
||||
if (controller.widget.selectedView == null) return;
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
if (controller.currentLemma != null)
|
||||
if (controller.widget.selectedView != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.currentLemma != null)
|
||||
if (controller.widget.selectedView != null)
|
||||
TextSpan(
|
||||
text: controller.currentLemma,
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
text: controller.widget.selectedView!.string(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: controller.navigate,
|
||||
),
|
||||
actions: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: controller.selectedView != null
|
||||
child: controller.widget.selectedView != null
|
||||
? Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.student)
|
||||
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,29 +165,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(
|
||||
AnalyticsSelected(
|
||||
|
|
@ -188,35 +189,35 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
.widget
|
||||
.tabs[0]
|
||||
.allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
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,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -226,36 +227,26 @@ 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,
|
||||
controller: controller,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -275,7 +266,7 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
children: [
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: const Text("Error Analytics"),
|
||||
title: Text(L10n.of(context)!.grammarAnalytics),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
|
|
@ -284,13 +275,20 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
child: Icon(BarChartViewSelection.grammar.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => controller.setSelectedView(
|
||||
BarChartViewSelection.grammar,
|
||||
),
|
||||
onTap: () {
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
route += "/${BarChartViewSelection.grammar.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: const Text("Message Analytics"),
|
||||
title: Text(L10n.of(context)!.messageAnalytics),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
|
|
@ -299,9 +297,16 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
child: Icon(BarChartViewSelection.messages.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => controller.setSelectedView(
|
||||
BarChartViewSelection.messages,
|
||||
),
|
||||
onTap: () {
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
route += "/${BarChartViewSelection.messages.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
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/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/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,92 +13,71 @@ 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});
|
||||
final BarChartViewSelection? selectedView;
|
||||
const ClassAnalyticsPage({super.key, this.selectedView});
|
||||
|
||||
@override
|
||||
State<ClassAnalyticsPage> createState() => ClassAnalyticsV2Controller();
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
if (_classRoom == null) {
|
||||
context.go('/rooms/analytics');
|
||||
}
|
||||
getChatAndStudents();
|
||||
}
|
||||
return _classRoom;
|
||||
}
|
||||
|
||||
String className(BuildContext context) {
|
||||
return classRoom?.name ?? "";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint("init class analytics");
|
||||
Future.delayed(Duration.zero, () async {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getChatAndStudents() async {
|
||||
try {
|
||||
await classRoom?.postLoad();
|
||||
await classRoom?.requestParticipants();
|
||||
|
||||
if (classRoom != null) {
|
||||
final response = await Matrix.of(context).client.getSpaceHierarchy(
|
||||
classRoom!.id,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
// set the latest fetched full hierarchy in message analytics controller
|
||||
// we want to avoid calling this endpoint again and again, so whenever the
|
||||
// data is made available, set it in the controller
|
||||
MatrixState.pangeaController.analytics
|
||||
.setLatestHierarchy(_classRoom!.id, response);
|
||||
|
||||
students = classRoom!.students;
|
||||
chats = response.rooms
|
||||
.where(
|
||||
|
|
@ -122,21 +98,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 +113,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,18 +48,18 @@ class ClassAnalyticsView extends StatelessWidget {
|
|||
|
||||
return controller.classId != null
|
||||
? BaseAnalyticsPage(
|
||||
selectedView: controller.widget.selectedView,
|
||||
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,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.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 '../../../models/analytics/chart_analytics_model.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../../../widgets/common/list_placeholder.dart';
|
||||
|
||||
|
|
@ -22,75 +23,57 @@ class AnalyticsClassList extends StatefulWidget {
|
|||
class AnalyticsClassListController extends State<AnalyticsClassList> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
List<ChartAnalyticsModel> models = [];
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Map<String, Timer> refreshTimer = {};
|
||||
List<Room> spaces = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
stateSub = pangeaController.matrixState.client.onRoomState.stream
|
||||
Matrix.of(context).client.classesAndExchangesImTeaching.then((spaceList) {
|
||||
spaceList = spaceList
|
||||
.where(
|
||||
(event) => event.type == PangeaEventTypes.studentAnalyticsSummary,
|
||||
(space) => !spaceList.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.listen(onStateUpdate);
|
||||
.toList();
|
||||
spaces = spaceList;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return PLoadingStatusV2(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../enum/time_span.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'class_list.dart';
|
||||
|
|
@ -45,24 +43,28 @@ class AnalyticsClassListView extends StatelessWidget {
|
|||
body: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FutureBuilder(
|
||||
future: Matrix.of(context).client.classesAndExchangesImTeaching,
|
||||
builder: (context, snapshot) => ListView.builder(
|
||||
itemCount: snapshot.hasData ? snapshot.data?.length ?? 0 : 0,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
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,
|
||||
onTap: (selected) => context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
),
|
||||
allowNavigateOnSelect: true,
|
||||
selected: false,
|
||||
child: ListView.builder(
|
||||
itemCount: controller.spaces.length,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.spaces[i].id,
|
||||
AnalyticsEntryType.space,
|
||||
"",
|
||||
),
|
||||
avatar: controller.spaces[i].avatar,
|
||||
selected: AnalyticsSelected(
|
||||
controller.spaces[i].id,
|
||||
AnalyticsEntryType.space,
|
||||
controller.spaces[i].name,
|
||||
),
|
||||
onTap: (selected) {
|
||||
context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
);
|
||||
},
|
||||
allowNavigateOnSelect: true,
|
||||
isSelected: false,
|
||||
pangeaController: controller.pangeaController,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ 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';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
@ -40,29 +41,9 @@ class ConstructList extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ConstructListState extends State<ConstructList> {
|
||||
bool initialized = false;
|
||||
String? langCode;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.pangeaController.analytics
|
||||
.setConstructs(
|
||||
constructType: widget.constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.whenComplete(() => setState(() => initialized = true));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return error != null
|
||||
|
|
@ -72,11 +53,11 @@ class ConstructListState extends State<ConstructList> {
|
|||
: Column(
|
||||
children: [
|
||||
ConstructListView(
|
||||
init: initialized,
|
||||
controller: widget.controller,
|
||||
pangeaController: widget.pangeaController,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
refreshStream: widget.refreshStream,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -93,19 +74,18 @@ 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,
|
||||
required this.init,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.defaultSelected,
|
||||
required this.refreshStream,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
|
|
@ -114,59 +94,55 @@ class ConstructListView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ConstructListViewState extends State<ConstructListView> {
|
||||
final ConstructType constructType = ConstructType.grammar;
|
||||
final Map<String, Timeline> _timelinesCache = {};
|
||||
final Map<String, PangeaMessageEvent> _msgEventCache = {};
|
||||
final List<PangeaMessageEvent> _msgEvents = [];
|
||||
bool fetchingConstructs = true;
|
||||
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,
|
||||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
);
|
||||
await fetchUses();
|
||||
},
|
||||
);
|
||||
)
|
||||
.whenComplete(() => setState(() => fetchingConstructs = false))
|
||||
.then((value) => setState(() => _constructs = value));
|
||||
|
||||
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
|
||||
// postframe callback to let widget rebuild with the new selected parameter
|
||||
// before sending selected to getConstructs function
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.then(
|
||||
(value) => setState(() {
|
||||
_constructs = value;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@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,18 +217,47 @@ 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<ConstructAnalyticsEvent>? _constructs;
|
||||
|
||||
AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
List<ConstructUses>? get constructs {
|
||||
if (_constructs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<OneConstructUse> filtered = List.from(_constructs!)
|
||||
.map((event) => event.content.uses)
|
||||
.expand((uses) => uses)
|
||||
.cast<OneConstructUse>()
|
||||
.where((use) => use.constructType == constructType)
|
||||
.toList();
|
||||
|
||||
final Map<String, List<OneConstructUse>> lemmaToUses = {};
|
||||
for (final use in filtered) {
|
||||
if (use.lemma == null) continue;
|
||||
lemmaToUses[use.lemma!] ??= [];
|
||||
lemmaToUses[use.lemma!]!.add(use);
|
||||
}
|
||||
|
||||
final constructUses = lemmaToUses.entries
|
||||
.map(
|
||||
(entry) => ConstructUses(
|
||||
lemma: entry.key,
|
||||
uses: entry.value,
|
||||
constructType: constructType,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
constructUses.sort((a, b) {
|
||||
final comp = b.uses.length.compareTo(a.uses.length);
|
||||
if (comp != 0) return comp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
});
|
||||
|
||||
return constructUses;
|
||||
}
|
||||
|
||||
ConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
);
|
||||
|
||||
|
|
@ -297,13 +302,13 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.init || fetchingUses) {
|
||||
if (fetchingConstructs || fetchingUses) {
|
||||
return const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if ((constructs?.isEmpty ?? true) ||
|
||||
(widget.controller.currentLemma != null && currentConstruct == null)) {
|
||||
|
||||
if (constructs?.isEmpty ?? true) {
|
||||
return Expanded(
|
||||
child: Center(child: Text(L10n.of(context)!.noDataFound)),
|
||||
);
|
||||
|
|
@ -341,7 +346,10 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.controller.currentLemma == null) {
|
||||
if (controller.widget.controller.currentLemma == null ||
|
||||
controller.constructs == null ||
|
||||
controller.lemmaIndex < 0 ||
|
||||
controller.lemmaIndex >= controller.constructs!.length) {
|
||||
return const AlertDialog(content: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
|
|
@ -349,38 +357,40 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
return AlertDialog(
|
||||
title: Center(child: Text(controller.widget.controller.currentLemma!)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context)!.roomDataMissing),
|
||||
content: SizedBox(
|
||||
height: 350,
|
||||
width: 500,
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context)!.roomDataMissing),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
...msgEventMatches.mapIndexed(
|
||||
(index, event) => Column(
|
||||
children: [
|
||||
ConstructMessage(
|
||||
msgEvent: event.msgEvent,
|
||||
lemma: controller.widget.controller.currentLemma!,
|
||||
errorMessage: event.lemmaMatch,
|
||||
),
|
||||
if (index < msgEventMatches.length - 1)
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...msgEventMatches.mapIndexed(
|
||||
(index, event) => Column(
|
||||
children: [
|
||||
ConstructMessage(
|
||||
msgEvent: event.msgEvent,
|
||||
lemma: controller.widget.controller.currentLemma!,
|
||||
errorMessage: event.lemmaMatch,
|
||||
),
|
||||
if (index < msgEventMatches.length - 1)
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
@ -474,21 +484,21 @@ class ConstructMessage extends StatelessWidget {
|
|||
class ConstructMessageBubble extends StatelessWidget {
|
||||
final String errorText;
|
||||
final String replacementText;
|
||||
final int? start;
|
||||
final int? end;
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
const ConstructMessageBubble({
|
||||
super.key,
|
||||
required this.errorText,
|
||||
required this.replacementText,
|
||||
this.start,
|
||||
this.end,
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyle = TextStyle(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
height: 1.3,
|
||||
);
|
||||
|
|
@ -516,7 +526,7 @@ class ConstructMessageBubble extends StatelessWidget {
|
|||
vertical: 8,
|
||||
),
|
||||
child: RichText(
|
||||
text: (start == null || end == null)
|
||||
text: (end == null)
|
||||
? TextSpan(
|
||||
text: errorText,
|
||||
style: defaultStyle,
|
||||
|
|
@ -528,7 +538,7 @@ class ConstructMessageBubble extends StatelessWidget {
|
|||
style: defaultStyle,
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(start!, end),
|
||||
text: errorText.substring(start, end),
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.red.withOpacity(0.25),
|
||||
|
|
@ -547,7 +557,7 @@ class ConstructMessageBubble extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(end!),
|
||||
text: errorText.substring(end),
|
||||
style: defaultStyle,
|
||||
),
|
||||
],
|
||||
|
|
@ -569,14 +579,7 @@ class ConstructMessageMetadata extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String roomName = msgEvent.event.room.name.isEmpty
|
||||
? Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(msgEvent.event.room.id)
|
||||
?.getLocalizedDisplayname() ??
|
||||
""
|
||||
: msgEvent.event.room.name;
|
||||
|
||||
final String roomName = msgEvent.event.room.getLocalizedDisplayname();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 30, 0),
|
||||
child: Column(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/models/chart_analytics_model.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
|
||||
class ListSummaryAnalytics extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:intl/intl.dart';
|
|||
|
||||
import '../../enum/time_span.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/chart_analytics_model.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
import 'bar_chart_card.dart';
|
||||
import 'messages_legend_widget.dart';
|
||||
|
||||
|
|
@ -58,10 +58,10 @@ class MessagesBarChartState extends State<MessagesBarChart> {
|
|||
getTitlesWidget: leftTitles,
|
||||
),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chart_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -11,13 +9,13 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../extensions/client_extension/client_extension.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics_view.dart';
|
||||
|
||||
class StudentAnalyticsPage extends StatefulWidget {
|
||||
const StudentAnalyticsPage({super.key});
|
||||
final BarChartViewSelection? selectedView;
|
||||
const StudentAnalyticsPage({super.key, this.selectedView});
|
||||
|
||||
@override
|
||||
State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
|
||||
|
|
@ -26,37 +24,55 @@ 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 = [];
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer?.isActive ?? false)) {
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => getClassAndChatAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
final listFutures = [
|
||||
_pangeaController.myAnalytics.setStudentChats(),
|
||||
_pangeaController.myAnalytics.setStudentSpaces(),
|
||||
];
|
||||
Future.wait(listFutures).then((_) => setState(() {}));
|
||||
|
||||
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((_) {
|
||||
if (_pangeaController.myAnalytics.studentChats.isNotEmpty) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentChats;
|
||||
}
|
||||
|
||||
List<Room> get spaces {
|
||||
if (_pangeaController.myAnalytics.studentSpaces.isEmpty) {
|
||||
_pangeaController.myAnalytics.setStudentSpaces().then((_) {
|
||||
if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentSpaces;
|
||||
}
|
||||
|
||||
String? get userId {
|
||||
final id = _pangeaController.matrixState.client.userID;
|
||||
debugger(when: kDebugMode && id == null);
|
||||
return id;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -66,96 +82,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,
|
||||
|
|
@ -45,9 +45,9 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
|
||||
return controller.userId != null
|
||||
? BaseAnalyticsPage(
|
||||
selectedView: controller.widget.selectedView,
|
||||
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,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ClassDescriptionButton extends StatelessWidget {
|
||||
final Room room;
|
||||
|
|
@ -20,21 +20,19 @@ class ClassDescriptionButton extends StatelessWidget {
|
|||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? controller.setTopicAction
|
||||
: null,
|
||||
leading: room.canSendEvent(EventTypes.RoomTopic)
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.topic_outlined),
|
||||
)
|
||||
: null,
|
||||
onTap: room.isRoomAdmin ? controller.setTopicAction : null,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.topic_outlined),
|
||||
),
|
||||
subtitle: Text(
|
||||
room.topic.isEmpty
|
||||
? (room.isSpace
|
||||
? L10n.of(context)!.classDescriptionDesc
|
||||
: L10n.of(context)!.chatTopicDesc)
|
||||
? (room.isRoomAdmin
|
||||
? (room.isSpace
|
||||
? L10n.of(context)!.classDescriptionDesc
|
||||
: L10n.of(context)!.chatTopicDesc)
|
||||
: L10n.of(context)!.topicNotSet)
|
||||
: room.topic,
|
||||
),
|
||||
title: Text(
|
||||
|
|
@ -51,3 +49,53 @@ class ClassDescriptionButton extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setClassTopic(Room room, BuildContext context) {
|
||||
final TextEditingController textFieldController =
|
||||
TextEditingController(text: room.topic);
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(
|
||||
room.isSpace
|
||||
? L10n.of(context)!.classDescription
|
||||
: L10n.of(context)!.chatTopic,
|
||||
),
|
||||
content: TextField(
|
||||
controller: textFieldController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
maxLength: 2000,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.ok),
|
||||
onPressed: () async {
|
||||
if (textFieldController.text == "") return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setDescription(textFieldController.text),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text(L10n.of(context)!.groupDescriptionHasBeenChanged),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class RoomCapacityButton extends StatefulWidget {
|
||||
final Room? room;
|
||||
final ChatDetailsController? controller;
|
||||
const RoomCapacityButton({
|
||||
super.key,
|
||||
this.room,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
RoomCapacityButtonState createState() => RoomCapacityButtonState();
|
||||
}
|
||||
|
||||
class RoomCapacityButtonState extends State<RoomCapacityButton> {
|
||||
int? capacity;
|
||||
String? nonAdmins;
|
||||
|
||||
RoomCapacityButtonState({Key? key});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
capacity = widget.room?.capacity;
|
||||
widget.room?.numNonAdmins.then(
|
||||
(value) => setState(() {
|
||||
nonAdmins = value.toString();
|
||||
overCapacity();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RoomCapacityButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.room != widget.room) {
|
||||
capacity = widget.room?.capacity;
|
||||
widget.room?.numNonAdmins.then(
|
||||
(value) => setState(() {
|
||||
nonAdmins = value.toString();
|
||||
overCapacity();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> overCapacity() async {
|
||||
if ((widget.room?.isRoomAdmin ?? false) &&
|
||||
capacity != null &&
|
||||
nonAdmins != null &&
|
||||
int.parse(nonAdmins!) > capacity!) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context)!.roomExceedsCapacity,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () =>
|
||||
((widget.room?.isRoomAdmin ?? true) ? (setRoomCapacity()) : null),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.reduce_capacity),
|
||||
),
|
||||
subtitle: Text(
|
||||
(capacity == null)
|
||||
? L10n.of(context)!.capacityNotSet
|
||||
: (nonAdmins != null)
|
||||
? '$nonAdmins/$capacity'
|
||||
: '$capacity',
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.roomCapacity,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setCapacity(int newCapacity) async {
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
Future<void> setRoomCapacity() async {
|
||||
final input = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.roomCapacity,
|
||||
message: L10n.of(context)!.roomCapacityExplanation,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
initialText: ((capacity != null) ? '$capacity' : ''),
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 3,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
int.tryParse(value) == null ||
|
||||
int.parse(value) < 0) {
|
||||
return L10n.of(context)!.enterNumber;
|
||||
}
|
||||
if (nonAdmins != null && int.parse(value) < int.parse(nonAdmins!)) {
|
||||
return L10n.of(context)!.capacitySetTooLow;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null ||
|
||||
input.first == "" ||
|
||||
int.tryParse(input.first) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newCapacity = int.parse(input.first);
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => ((widget.room != null)
|
||||
? (widget.room!.updateRoomCapacity(
|
||||
capacity = newCapacity,
|
||||
))
|
||||
: setCapacity(newCapacity)),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context)!.roomCapacityHasBeenChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/enum/span_choice_type.dart';
|
||||
import 'package:fluffychat/pangea/enum/span_data_type.dart';
|
||||
|
|
@ -80,15 +81,39 @@ class SpanDetailsRepoReqAndRes {
|
|||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SpanDetailsRepoReqAndRes) return false;
|
||||
|
||||
return toJson().toString() == other.toJson().toString();
|
||||
if (other.userL1 != userL1) return false;
|
||||
if (other.userL2 != userL2) return false;
|
||||
if (other.enableIT != enableIT) return false;
|
||||
if (other.enableIGC != enableIGC) return false;
|
||||
if (other.span.choices
|
||||
?.firstWhere(
|
||||
(choice) => choice.type == SpanChoiceType.bestCorrection,
|
||||
)
|
||||
.value !=
|
||||
span.choices
|
||||
?.firstWhere(
|
||||
(choice) => choice.type == SpanChoiceType.bestCorrection,
|
||||
)
|
||||
.value) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Overrides the hashCode getter to generate a hash code for the [SpanDetailsRepoReqAndRes] object.
|
||||
/// Used as keys in response cache in igc_controller.
|
||||
@override
|
||||
int get hashCode {
|
||||
return toJson().toString().hashCode;
|
||||
return Object.hashAll([
|
||||
userL1.hashCode,
|
||||
userL2.hashCode,
|
||||
enableIT.hashCode,
|
||||
enableIGC.hashCode,
|
||||
span.choices
|
||||
?.firstWhereOrNull(
|
||||
(choice) => choice.type == SpanChoiceType.bestCorrection,
|
||||
)
|
||||
?.value
|
||||
.hashCode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@ class BotStyle {
|
|||
AppConfig.fontSizeFactor *
|
||||
(big == true ? 1.2 : 1),
|
||||
fontStyle: italics ? FontStyle.italic : null,
|
||||
color: setColor
|
||||
? Theme.of(context).brightness == Brightness.dark
|
||||
? AppConfig.primaryColorLight
|
||||
: AppConfig.primaryColor
|
||||
: null,
|
||||
color: setColor ? Theme.of(context).colorScheme.primary : null,
|
||||
inherit: true,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ void chatListHandleSpaceTap(
|
|||
context: context,
|
||||
future: () async {
|
||||
await space.join();
|
||||
if (await space.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
await space.postLoad();
|
||||
setActiveSpaceAndCloseChat();
|
||||
},
|
||||
|
|
@ -65,6 +68,9 @@ void chatListHandleSpaceTap(
|
|||
context: context,
|
||||
future: () async {
|
||||
await space.join();
|
||||
if (await space.leaveIfFull()) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
if (space.isSpace) {
|
||||
await space.joinAnalyticsRoomsInSpace();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class ClassChatPowerLevels {
|
|||
final Map<String, dynamic> powerLevelOverride = {};
|
||||
powerLevelOverride['events'] = {
|
||||
EventTypes.spaceChild: 0,
|
||||
PangeaEventTypes.studentAnalyticsSummary: 0,
|
||||
};
|
||||
powerLevelOverride['users'] = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
void setClassTopic(Room room, BuildContext context) {
|
||||
final TextEditingController textFieldController =
|
||||
TextEditingController(text: room.topic);
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text(
|
||||
room.isSpace
|
||||
? L10n.of(context)!.classDescription
|
||||
: L10n.of(context)!.chatTopic,
|
||||
),
|
||||
content: TextField(
|
||||
controller: textFieldController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
maxLength: 2000,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.of(context)!.ok),
|
||||
onPressed: () async {
|
||||
if (textFieldController.text == "") return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setDescription(textFieldController.text),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text(L10n.of(context)!.groupDescriptionHasBeenChanged),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -47,6 +47,10 @@ class OverlayMessage extends StatelessWidget {
|
|||
}
|
||||
|
||||
var color = Theme.of(context).colorScheme.surfaceVariant;
|
||||
// #Pangea
|
||||
final isLight = Theme.of(context).brightness == Brightness.light;
|
||||
var lightness = isLight ? .05 : .85;
|
||||
// Pangea#
|
||||
final textColor = ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground;
|
||||
|
|
@ -98,7 +102,21 @@ class OverlayMessage extends StatelessWidget {
|
|||
|
||||
if (ownMessage) {
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
lightness = isLight ? .15 : .85;
|
||||
}
|
||||
// Make overlay a little darker/lighter than the message
|
||||
color = Color.fromARGB(
|
||||
color.alpha,
|
||||
isLight
|
||||
? (color.red + lightness * (255 - color.red)).round()
|
||||
: (color.red * lightness).round(),
|
||||
isLight
|
||||
? (color.green + lightness * (255 - color.green)).round()
|
||||
: (color.green * lightness).round(),
|
||||
isLight
|
||||
? (color.blue + lightness * (255 - color.blue)).round()
|
||||
: (color.blue * lightness).round(),
|
||||
);
|
||||
|
||||
// #Pangea
|
||||
final pangeaMessageEvent = PangeaMessageEvent(
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ abstract class ClientManager {
|
|||
// #Pangea
|
||||
PangeaEventTypes.classSettings,
|
||||
PangeaEventTypes.rules,
|
||||
PangeaEventTypes.vocab,
|
||||
PangeaEventTypes.botOptions,
|
||||
EventTypes.RoomTopic,
|
||||
EventTypes.RoomAvatar,
|
||||
PangeaEventTypes.capacity,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -9,11 +14,6 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:punycode/punycode.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import 'platform_infos.dart';
|
||||
|
||||
class UrlLauncher {
|
||||
|
|
@ -159,7 +159,10 @@ class UrlLauncher {
|
|||
room = matrix.client.getRoomById(roomId!);
|
||||
}
|
||||
servers.addAll(identityParts.via);
|
||||
if (room != null) {
|
||||
// #Pangea
|
||||
if (room != null && room.membership != Membership.leave) {
|
||||
// if (room != null) {
|
||||
// Pangea#
|
||||
if (room.isSpace) {
|
||||
// TODO: Implement navigate to space
|
||||
context.go('/rooms/${room.id}');
|
||||
|
|
@ -202,7 +205,19 @@ class UrlLauncher {
|
|||
serverName: servers.isNotEmpty ? servers.toList() : null,
|
||||
),
|
||||
);
|
||||
if (response.error != null) return;
|
||||
// #Pangea
|
||||
// if (response.error != null) return;
|
||||
if (response.error != null ||
|
||||
(room != null && (await room.leaveIfFull()))) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 10),
|
||||
content: Text(L10n.of(context)!.roomFull),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
// wait for two seconds so that it probably came down /sync
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../utils/localized_exception_extension.dart';
|
||||
|
||||
class PublicRoomBottomSheet extends StatelessWidget {
|
||||
|
|
@ -44,6 +44,12 @@ class PublicRoomBottomSheet extends StatelessWidget {
|
|||
if (client.getRoomById(roomId) == null) {
|
||||
await client.waitForRoomInSync(roomId);
|
||||
}
|
||||
// #Pangea
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && (await room.leaveIfFull())) {
|
||||
throw L10n.of(context)!.roomFull;
|
||||
}
|
||||
// Pangea#
|
||||
return roomId;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -850,7 +850,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"be": [
|
||||
|
|
@ -2299,7 +2310,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
|
|
@ -3210,7 +3232,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"bo": [
|
||||
|
|
@ -4121,7 +4154,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
|
|
@ -5032,7 +5076,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
|
|
@ -5943,7 +5998,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"de": [
|
||||
|
|
@ -6801,7 +6867,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
|
@ -7712,7 +7789,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"eo": [
|
||||
|
|
@ -8623,24 +8711,22 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"suggestToChat",
|
||||
"suggestToChatDesc",
|
||||
"autoIGCToolName",
|
||||
"autoIGCToolDescription",
|
||||
"runGrammarCorrection",
|
||||
"grammarCorrectionFailed",
|
||||
"grammarCorrectionComplete",
|
||||
"leaveRoomDescription",
|
||||
"archiveSpaceDescription",
|
||||
"leaveSpaceDescription",
|
||||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"addSpaceToSpaceDescription"
|
||||
],
|
||||
|
||||
"et": [
|
||||
|
|
@ -9494,7 +9580,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
|
|
@ -10348,7 +10445,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
|
|
@ -11259,7 +11367,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
|
|
@ -12170,7 +12289,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
|
@ -13081,7 +13211,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ga": [
|
||||
|
|
@ -13992,7 +14133,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
|
|
@ -14846,7 +14998,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"he": [
|
||||
|
|
@ -15757,7 +15920,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
|
|
@ -16668,7 +16842,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"hr": [
|
||||
|
|
@ -17566,7 +17751,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"hu": [
|
||||
|
|
@ -18477,7 +18673,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ia": [
|
||||
|
|
@ -19912,7 +20119,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"id": [
|
||||
|
|
@ -20823,7 +21041,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ie": [
|
||||
|
|
@ -21734,7 +21963,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
|
@ -22630,7 +22870,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
|
|
@ -23541,7 +23792,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
|
|
@ -24452,7 +24714,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
|
|
@ -25363,7 +25636,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"lv": [
|
||||
|
|
@ -26274,7 +26558,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"nb": [
|
||||
|
|
@ -27185,7 +27480,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
|
@ -28096,7 +28402,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
|
|
@ -29007,7 +29324,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
|
@ -29918,7 +30246,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"pt_BR": [
|
||||
|
|
@ -30798,7 +31137,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"pt_PT": [
|
||||
|
|
@ -31709,7 +32059,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ro": [
|
||||
|
|
@ -32620,7 +32981,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
|
@ -33474,7 +33846,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"sk": [
|
||||
|
|
@ -34385,7 +34768,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
|
|
@ -35296,7 +35690,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"sr": [
|
||||
|
|
@ -36207,7 +36612,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"sv": [
|
||||
|
|
@ -37083,7 +37499,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"ta": [
|
||||
|
|
@ -37994,7 +38421,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"th": [
|
||||
|
|
@ -38905,7 +39343,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
|
|
@ -39801,7 +40250,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
|
|
@ -40655,7 +41115,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
|
|
@ -41566,7 +42037,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
|
@ -42420,7 +42902,18 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
|
@ -43331,6 +43824,17 @@
|
|||
"onlyAdminDescription",
|
||||
"tooltipInstructionsTitle",
|
||||
"tooltipInstructionsMobileBody",
|
||||
"tooltipInstructionsBrowserBody"
|
||||
"tooltipInstructionsBrowserBody",
|
||||
"addSpaceToSpaceDescription",
|
||||
"roomCapacity",
|
||||
"roomFull",
|
||||
"topicNotSet",
|
||||
"capacityNotSet",
|
||||
"roomCapacityHasBeenChanged",
|
||||
"roomExceedsCapacity",
|
||||
"capacitySetTooLow",
|
||||
"roomCapacityExplanation",
|
||||
"enterNumber",
|
||||
"buildTranslation"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue