Merge branch 'main' into only-replace-correct

This commit is contained in:
ggurdin 2024-06-14 13:32:13 -04:00 committed by GitHub
commit cec3d7f710
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 7344 additions and 6709 deletions

File diff suppressed because it is too large Load diff

View file

@ -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"
}

View file

@ -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,
),
),
),
],
),
],
),

View file

@ -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),
);

View file

@ -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,

View file

@ -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;

View file

@ -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';

View file

@ -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,

View file

@ -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();

View file

@ -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,

View file

@ -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: [

View file

@ -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

View file

@ -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;
},

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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();
}

View file

@ -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';
}

View file

@ -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";
}

View file

@ -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";

View file

@ -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

View file

@ -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();
}

View file

@ -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}",
);
}
}
}
}

View file

@ -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';
}
}
}

View file

@ -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 }

View file

@ -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();
}

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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 [];
}
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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;

View file

@ -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(),
// );
// }
// }
// }

View 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;
}
}

View 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 [];
}
}

View file

@ -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,
);
}
}

View 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;
}
}

View file

@ -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,
});
}

View 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;
}
}

View file

@ -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]),
);
}
}

View file

@ -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

View file

@ -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]),
// );
// }

View file

@ -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;
// }
// }

View file

@ -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}",
),
);
}

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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() {}
// }

View file

@ -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,
),
),
);

View file

@ -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;

View file

@ -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),
],

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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,
),
),
),

View file

@ -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(

View file

@ -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 {

View file

@ -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),
),
);

View file

@ -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 ?? "";
}

View file

@ -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,

View file

@ -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,

View file

@ -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();
}
},
),
],
),
);
}

View file

@ -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(() {});
}
}
}

View file

@ -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,
]);
}
}

View file

@ -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,
);

View file

@ -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();
}

View file

@ -15,7 +15,6 @@ class ClassChatPowerLevels {
final Map<String, dynamic> powerLevelOverride = {};
powerLevelOverride['events'] = {
EventTypes.spaceChild: 0,
PangeaEventTypes.studentAnalyticsSummary: 0,
};
powerLevelOverride['users'] = {};

View file

@ -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();
}
},
),
],
),
);
}

View file

@ -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(

View file

@ -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,

View file

@ -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,

View file

@ -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;
},
);

View file

@ -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"
]
}