diff --git a/README.md b/README.md index 7c4970e00..7c27b6e2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -Pangea Chat Client Setup: +# Overview -* Download VSCode if you do not already have it installed +[Pangea Chat](https://pangea.chat) is a web and mobile platform which lets students ‘learn a language while texting their friends.’ Addressing the gap in communicative language teaching, especially for beginners lacking skill and confidence, Pangea Chat provides a low-stress, high-support environment for language learning through authentic conversations. By integrating human and artificial intelligence, the app enhances communicative abilities and supports educators. Pangea Chat has been grant funded by the National Science Foundation and Virginia Innovation Partnership Corporation based on its technical innovation and potential for broad social impact. Our mission is to build a global, decentralized learning network supporting intercultural learning and exchange. + +# Pangea Chat Client Setup + +* Download VSCode if you do not already have it installed. This is the preferred IDE for development with Pangea Chat. * Download flutter on your device using this guide: https://docs.flutter.dev/get-started/install * Test to make sure that flutter is properly installed by running “flutter –version” * You may need to add flutter to your path manually. Instructions can be found here: https://docs.flutter.dev/get-started/install/macos/mobile-ios?tab=download#add-flutter-to-your-path @@ -14,7 +18,7 @@ Pangea Chat Client Setup: * Run “brew install cocoapods” to install cocoapods * Run “flutter doctor” and for any missing components, follow the instructions from the print out to install / setup * Clone the client repo -* Copy the .env file (and the .env.prod file, if you want to run production builds), into the root folder of the client and the assets/ folder +* Copy the .env file (and the .env.prod file, if you want to run production builds), into the root folder of the client and the assets/ folder. Contact Gabby for a copy of this file. * Uncomment the lines in the pubspec.yaml file in the assets section with paths to .env file * To run on iOS: * Run “flutter precache --ios” @@ -25,62 +29,10 @@ Pangea Chat Client Setup: * On web, run `flutter run -d chrome –hot` * On mobile device or simulator, run `flutter run –hot -d ` -![Screenshot](https://github.com/krille-chan/fluffychat/blob/main/assets/banner_transparent.png?raw=true) - -[FluffyChat](https://fluffychat.im) is an open source, nonprofit and cute [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). The goal of the app is to create an easy to use instant messenger which is open source and accessible for everyone. - -### Links: - -- 🌐 [[Weblate] Translate FluffyChat into your language](https://hosted.weblate.org/projects/fluffychat/) -- 🌍 [[m] Join the community](https://matrix.to/#/#fluffychat:matrix.org) -- 📰 [[Mastodon] Get updates on social media](https://mastodon.art/@krille) -- 🖥️ [[Famedly] Server hosting and professional support](https://famedly.com/kontakt) -- 💝 [[Liberapay] Support FluffyChat development](https://de.liberapay.com/KrilleChritzelius) - -Buy Me a Coffee at ko-fi.com - -### Screenshots: - -![Screenshot](https://github.com/krille-chan/fluffychat/blob/main/docs/screenshots/product.jpeg?raw=true) - -# Features - -- 📩 Send all kinds of messages, images and files -- 🎙️ Voice messages -- 📍 Location sharing -- 🔔 Push notifications -- 💬 Unlimited private and public group chats -- 📣 Public channels with thousands of participants -- 🛠️ Feature rich group moderation including all matrix features -- 🔍 Discover and join public groups -- 🌙 Dark mode -- 🎨 Material You design -- 📟 Hides complexity of Matrix IDs behind simple QR codes -- 😄 Custom emotes and stickers -- 🌌 Spaces -- 🔄 Compatible with Element, Nheko, NeoChat and all other Matrix apps -- 🔐 End to end encryption -- 🔒 Encrypted chat backup -- 😀 Emoji verification & cross signing - -... and much more. - - -# Installation - -Please visit the website for installation instructions: - -- https://fluffychat.im - -# How to build - -Please visit the [Wiki](https://github.com/krille-chan/fluffychat/wiki) for build instructions: - -- https://github.com/krille-chan/fluffychat/wiki/How-To-Build - - # Special thanks +* Pangea Chat is a fork of [FluffyChat](https://fluffychat.im), is an open source, nonprofit and cute [[matrix](https://matrix.org)] client written in [Flutter](https://flutter.dev). The goal of FluffyChat is to create an easy to use instant messenger which is open source and accessible for everyone. You can [support the primary maker of FluffyChat directly here.](https://ko-fi.com/C1C86VN53) + * Fabiyamada is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs. * Advocatux has made the Spanish translation with great love and care. He always stands by my side and supports my work with great commitment. diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index aba4b4ac3..fe2a3da03 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2504,7 +2504,7 @@ "type": "text", "placeholders": {} }, - "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Settings > Learning Settings.", + "interactiveTranslatorAllowedDesc": "Students can choose whether to use translation assistance in space group chats in Main Menu > My Learning Settings.", "@interactiveTranslatorAllowedDesc": { "type": "text", "placeholders": {} @@ -2984,9 +2984,9 @@ "errorDisableLanguageAssistanceClassDesc": "Translation assistance and grammar assistance are turned off for the space that this chat is in.", "itIsDisabled": "Interactive Translation is disabled", "igcIsDisabled": "Interactive Grammar Checking is disabled", - "goToLearningSettings": "Go to Learning Settings", + "goToLearningSettings": "Go to My Learning Settings", "error405Title": "Languages not set", - "error405Desc": "Please set your languages in Settings > Learning Settings", + "error405Desc": "Please set your languages in Main Menu > My Learning Settings.", "loginOrSignup": "Sign in with", "@loginOrSignup": { "type": "text", @@ -3049,7 +3049,7 @@ "type": "text", "placeholders": {} }, - "learningSettings": "Learning Settings", + "learningSettings": "My Learning Settings", "classNameRequired": "Please enter a space name", "@classNameRequired": { "type": "text", @@ -3674,7 +3674,7 @@ "bestAnswerFeedback": "That's correct!", "definitionDefaultPrompt": "What does this word mean?", "practiceDefaultPrompt": "What is the best answer?", - "correctionDefaultPrompt": "What is the best correction?", + "correctionDefaultPrompt": "What is the best replacement?", "itStartDefaultPrompt": "Do you want help translating?", "languageLevelWarning": "Please select a class language level", "lockedChatWarning": "🔒 This chat has been locked", @@ -3945,7 +3945,6 @@ "accuracy": "Accuracy", "points": "Points", "noPaymentInfo": "No payment info necessary!", - "updatePhoneOS": "You may need to update your device's OS version.", "conversationBotModeSelectDescription": "Bot mode", "conversationBotModeSelectOption_discussion": "Discussion", "conversationBotModeSelectOption_custom": "Custom", @@ -3960,5 +3959,9 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel": "Send discussion prompt on a schedule", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Send discussion prompt when user reacts ⏩ to bot message", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt", + "studentAnalyticsNotAvailable": "Student data not currently available", + "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", + "updatePhoneOS": "You may need to update your device's OS version.", + "wordsPerMinute": "Words per minute" } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index f9f596f16..59f5719b0 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4389,7 +4389,7 @@ "bestAnswerFeedback": "¡Correcto!", "definitionDefaultPrompt": "¿Qué significa esta palabra?", "practiceDefaultPrompt": "¿Cuál es la mejor respuesta?", - "correctionDefaultPrompt": "¿Cuál es la mejor corrección?", + "correctionDefaultPrompt": "¿Cuál es el mejor reemplazo?", "itStartDefaultPrompt": "¿Quiere ayuda para traducir?", "suggestTo": "Sugerir a {spaceName}", "suggestChatDesc": "Los chats sugeridos aparecerán en la lista de chats de {spaceName}.", @@ -4587,5 +4587,52 @@ "refresh": "Actualizar", "joinToView": "Únete a esta sala para ver los detalles", "autoPlayTitle": "Reproducción automática de mensajes", - "autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione." + "autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.", + "presenceStyle": "Presencia:", + "presencesToggle": "Mostrar mensajes de estado de otros usuarios", + "writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}...", + "@writeAMessageFlag": { + "type": "text", + "placeholders": { + "l1flag": {}, + "l2flag": {} + } + }, + "youInvitedToBy": "📩 Has sido invitado a través de un enlace a:\n{alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "hidePresences": "¿Ocultar la lista de estados?", + "sendReadReceipts": "Enviar recibos de lectura", + "sendTypingNotificationsDescription": "Los demás participantes en un chat pueden ver cuándo estás escribiendo un nuevo mensaje.", + "sendReadReceiptsDescription": "Los demás participantes en un chat pueden ver cuándo has leído un mensaje.", + "formattedMessages": "Mensajes con formato", + "formattedMessagesDescription": "Mostrar contenido de mensajes enriquecido como texto en negrita utilizando markdown.", + "verifyOtherUser": "🔐 Verificar otro usuario", + "verifyOtherUserDescription": "Si verificas a otro usuario, puedes estar seguro de saber a quién estás escribiendo realmente. 💪\n\nCuando inicies una verificación, tú y el otro usuario veréis una ventana emergente en la aplicación. Allí veréis una serie de emojis o números que tendréis que comparar entre vosotros.\n\nLa mejor forma de hacerlo es quedar o iniciar una videollamada. 👭", + "verifyOtherDevice": "🔐 Verificar otro dispositivo", + "verifyOtherDeviceDescription": "Cuando verificas otro dispositivo, esos dispositivos pueden intercambiar claves, aumentando tu seguridad general. 💪 Cuando inicies una verificación, aparecerá una ventana emergente en la app de ambos dispositivos. Allí verás entonces una serie de emojis o números que tienes que comparar entre sí. Lo mejor es que tengas ambos dispositivos a mano antes de iniciar la verificación. 🤳", + "transparent": "Transparente", + "incomingMessages": "Mensajes entrantes", + "stickers": "Pegatinas", + "commandHint_ignore": "Ignorar el ID de matriz dado", + "commandHint_unignore": "Designorar el ID de matriz dado", + "unreadChatsInApp": "{appname}: {unread} chats no leídos", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "messageAnalytics": "Análisis de mensajes", + "words": "Palabras", + "score": "Puntuación", + "accuracy": "Precisión", + "points": "Puntos", + "noPaymentInfo": "No se necesitan datos de pago.", + "updatePhoneOS": "Puede que necesites actualizar la versión del sistema operativo de tu dispositivo.", + "wordsPerMinute": "Palabras por minuto" } \ No newline at end of file diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 2b3e74f96..a21a2caad 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -29,6 +29,8 @@ abstract class AppConfig { static const Color primaryColorLight = Color(0xFFDBC9FF); static const Color secondaryColor = Color(0xFF41a2bc); static const Color activeToggleColor = Color(0xFF33D057); + static const Color success = Color(0xFF33D057); + static const Color warning = Color.fromARGB(255, 210, 124, 12); // static String _privacyUrl = // 'https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md'; static String _privacyUrl = "https://www.pangeachat.com/privacy"; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 8b69aca1e..e6ab37967 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -26,7 +26,6 @@ import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_style/settings_style.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/class_settings/class_settings_page.dart'; import 'package:fluffychat/pangea/pages/exchange/add_exchange_to_class.dart'; import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart'; import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart'; @@ -150,19 +149,6 @@ abstract class AppRoutes { : child, ), routes: [ - // #Pangea - GoRoute( - path: '/spaces/:roomid', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - ChatDetails( - roomId: state.pathParameters['roomid']!, - ), - ), - redirect: loggedOutRedirect, - ), - // Pangea# GoRoute( path: '/rooms', redirect: loggedOutRedirect, @@ -246,20 +232,26 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const NewGroup(), + NewGroup( + // #Pangea + spaceId: state.uri.queryParameters['spaceId'], + // Pangea# + ), ), redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: ':spaceid', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const NewGroup(), - ), - redirect: loggedOutRedirect, - ), - ], + // #Pangea + // routes: [ + // GoRoute( + // path: ':spaceid', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // state, + // const NewGroup(), + // ), + // redirect: loggedOutRedirect, + // ), + // ], + // Pangea# ), GoRoute( path: 'newspace', @@ -521,17 +513,6 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), - // #Pangea - GoRoute( - path: 'class_settings', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const ClassSettingsPage(), - ), - redirect: loggedOutRedirect, - ), - // Pangea# GoRoute( path: 'invite', pageBuilder: (context, state) => defaultPageBuilder( diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index a3c35c347..78e735d05 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -1,13 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/archive/archive_view.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/matrix.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 'package:fluffychat/pages/archive/archive_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class Archive extends StatefulWidget { const Archive({super.key}); @@ -20,7 +19,12 @@ class ArchiveController extends State { Future> getArchive(BuildContext context) async { if (archive.isNotEmpty) return archive; - return archive = await Matrix.of(context).client.loadArchive(); + // #Pangea + //return archive = await Matrix.of(context).client.loadArchive(); + return archive = (await Matrix.of(context).client.loadArchive()) + .where((e) => (!e.isSpace && !e.isAnalyticsRoom)) + .toList(); + // Pangea# } void forgetRoomAction(int i) async { diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 40701e742..460de198c 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -481,7 +481,16 @@ class ChatController extends State } // Do not send read markers when app is not in foreground - if (kIsWeb && !Matrix.of(context).webHasFocus) return; + // #Pangea + try { + // Pangea# + if (kIsWeb && !Matrix.of(context).webHasFocus) return; + // #Pangea + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + return; + } + // Pangea# if (!kIsWeb && WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { return; @@ -613,14 +622,14 @@ class ChatController extends State useType: useType, ) .then( - (String? msgEventId) { + (String? msgEventId) async { // #Pangea setState(() { if (previousEdit != null) { edittingEvents.add(previousEdit.eventId); } }); - // Pangea# + GoogleAnalytics.sendMessage( room.id, room.classCode, @@ -635,6 +644,8 @@ class ChatController extends State return; } + // ensure that analytics room exists / is created for the active langCode + await room.ensureAnalyticsRoomExists(); pangeaController.myAnalytics.handleMessage( room, RecentMessageRecord( @@ -1067,6 +1078,9 @@ class ChatController extends State bool get canEditSelectedEvents { if (isArchived || selectedEvents.length != 1 || + // #Pangea + selectedEvents.single.messageType != MessageTypes.Text || + // Pangea# !selectedEvents.first.status.isSent) { return false; } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0af014aef..fd8d95b5b 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -2,6 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -330,7 +331,12 @@ class ChatInputRow extends StatelessWidget { bottom: 6.0, top: 3.0, ), - hintText: activel1 != null && activel2 != null + hintText: activel1 != null && + activel2 != null && + activel1.langCode != + LanguageKeys.unknownLanguage && + activel2.langCode != + LanguageKeys.unknownLanguage ? L10n.of(context)!.writeAMessageFlag( activel1.languageEmoji ?? activel1.getDisplayName(context) ?? diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 57255ffc9..21789f527 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -116,7 +116,8 @@ class ChatView extends StatelessWidget { // #Pangea } else { return [ - ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat), + ChatSettingsPopupMenu(controller.room, + (!controller.room.isDirectChat && !controller.room.isArchived)), ]; } diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 373689043..472ef4eb4 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -167,9 +167,10 @@ class Message extends StatelessWidget { // #Pangea ToolbarDisplayController? toolbarController; if (event.type == EventTypes.Message && - event.messageType == MessageTypes.Text || - event.messageType == MessageTypes.Notice || - event.messageType == MessageTypes.Audio) { + !event.redacted && + (event.messageType == MessageTypes.Text || + event.messageType == MessageTypes.Notice || + event.messageType == MessageTypes.Audio)) { toolbarController = controller.getToolbarDisplayController( event.eventId, nextEvent: nextEvent, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 9866ddb2d..f04296d38 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -38,6 +38,7 @@ class MessageContent extends StatelessWidget { //further down in the chain is also using pangeaController so its not constant final bool immersionMode; final ToolbarDisplayController? toolbarController; + final bool isOverlay; // Pangea# const MessageContent( @@ -50,6 +51,7 @@ class MessageContent extends StatelessWidget { this.pangeaMessageEvent, required this.immersionMode, required this.toolbarController, + this.isOverlay = false, // Pangea# required this.borderRadius, }); @@ -203,7 +205,8 @@ class MessageContent extends StatelessWidget { && !(pangeaMessageEvent?.showRichText( selected, - toolbarController?.highlighted ?? false, + isOverlay: isOverlay, + highlighted: toolbarController?.highlighted ?? false, ) ?? false) // Pangea# @@ -305,7 +308,8 @@ class MessageContent extends StatelessWidget { ); if (pangeaMessageEvent?.showRichText( selected, - toolbarController?.highlighted ?? false, + isOverlay: isOverlay, + highlighted: toolbarController?.highlighted ?? false, ) ?? false) { return PangeaRichText( diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 9dbdc640c..2d46df11d 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -61,11 +61,22 @@ class ChatDetailsView extends StatelessWidget { ); return Scaffold( appBar: AppBar( - leading: controller.widget.embeddedCloseButton ?? - const Center(child: BackButton()), + leading: + // #Pangea + !room.isSpace + ? + // Pangea# + controller.widget.embeddedCloseButton ?? + const Center(child: BackButton()) + // #Pangea + : BackButton( + onPressed: () => context.go("/rooms"), + ) + // Pangea# + , elevation: Theme.of(context).appBarTheme.elevation, actions: [ - // #Pangeas + // #Pangea //if (room.canonicalAlias.isNotEmpty) // IconButton( // tooltip: L10n.of(context)!.share, diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index c39298d6c..c0791c936 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -6,7 +6,9 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/client_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/add_to_space.dart'; import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; @@ -521,7 +523,7 @@ class ChatListController extends State _invitedSpaceSubscription = pangeaController .matrixState.client.onSync.stream .where((event) => event.rooms?.invite != null) - .listen((event) { + .listen((event) async { for (final inviteEntry in event.rooms!.invite!.entries) { if (inviteEntry.value.inviteState == null) continue; final bool isSpace = inviteEntry.value.inviteState!.any( @@ -529,17 +531,39 @@ class ChatListController extends State event.type == EventTypes.RoomCreate && event.content['type'] == 'm.space', ); - if (!isSpace) continue; - final String spaceId = inviteEntry.key; - final Room? space = pangeaController.matrixState.client.getRoomById( - spaceId, + final bool isAnalytics = inviteEntry.value.inviteState!.any( + (event) => + event.type == EventTypes.RoomCreate && + event.content['type'] == PangeaRoomTypes.analytics, ); - if (space != null) { - chatListHandleSpaceTap( - context, - this, - space, + + if (isSpace) { + final String spaceId = inviteEntry.key; + final Room? space = pangeaController.matrixState.client.getRoomById( + spaceId, ); + if (space != null) { + chatListHandleSpaceTap( + context, + this, + space, + ); + } + } + + if (isAnalytics) { + final Room? analyticsRoom = + pangeaController.matrixState.client.getRoomById(inviteEntry.key); + try { + await analyticsRoom?.join(); + } catch (err, s) { + ErrorHandler.logError( + m: "Failed to join analytics room", + e: err, + s: s, + ); + } + return; } } }); @@ -704,6 +728,11 @@ class ChatListController extends State while (selectedRoomIds.isNotEmpty) { final roomId = selectedRoomIds.first; try { + // #Pangea + if (client.getRoomById(roomId)!.isUnread) { + await client.getRoomById(roomId)!.markUnread(false); + } + // Pangea# await client.getRoomById(roomId)!.leave(); } finally { toggleSelection(roomId); @@ -819,6 +848,7 @@ class ChatListController extends State pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); + await client.migrateAnalyticsRooms(); } else { ErrorHandler.logError( m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 447b5f6c3..9775ec624 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -53,6 +53,11 @@ class ChatListItem extends StatelessWidget { message: L10n.of(context)!.archiveRoomDescription, ); if (confirmed == OkCancelResult.cancel) return; + // #Pangea + if (room.isUnread) { + await room.markUnread(false); + } + // Pangea# await showFutureLoadingDialog( context: context, future: () => room.leave(), diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index dc8389db0..6ad9feef3 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -75,7 +75,12 @@ class ChatListView extends StatelessWidget { label: L10n.of(context)!.allChats, // Pangea# ), - if (controller.spaces.isNotEmpty) + if (controller.spaces.isNotEmpty + // #Pangea + && + !FluffyThemes.isColumnMode(context) + // Pangea# + ) // #Pangea // const NavigationDestination( // icon: Icon(Icons.workspaces_outlined), diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 896b6dc36..73b7497f6 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -69,7 +69,7 @@ class ClientChooserButton extends StatelessWidget { ), ), PopupMenuItem( - enabled: matrix.client.classesAndExchangesImIn.isNotEmpty, + enabled: matrix.client.allMyAnalyticsRooms.isNotEmpty, value: SettingsAction.myAnalytics, child: Row( children: [ @@ -154,6 +154,18 @@ class ClientChooserButton extends StatelessWidget { ], ), ), + // #Pangea + PopupMenuItem( + value: SettingsAction.learning, + child: Row( + children: [ + const Icon(Icons.psychology_outlined), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.learningSettings)), + ], + ), + ), + // Pangea# PopupMenuItem( value: SettingsAction.settings, child: Row( @@ -382,6 +394,9 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.setStatus: controller.setStatus(); // #Pangea + case SettingsAction.learning: + context.go('/rooms/settings/learning'); + break; case SettingsAction.newClass: context.go('/rooms/newspace'); break; @@ -493,6 +508,7 @@ enum SettingsAction { settings, archive, // #Pangea + learning, joinWithClassCode, classAnalytics, myAnalytics, diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index e7d793e3d..48be9eb04 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/extensions/sync_update_extension.dart'; import 'package:fluffychat/pangea/utils/archive_space.dart'; @@ -46,11 +48,23 @@ class _SpaceViewState extends State { // #Pangea StreamSubscription? _roomSubscription; bool refreshing = false; + + final String _chatCountsKey = 'chatCounts'; + Map get chatCounts => Map.from( + widget.controller.pangeaController.pStoreService.read( + _chatCountsKey, + local: true, + ) ?? + {}, + ); // Pangea# @override void initState() { loadHierarchy(); + // #Pangea + loadChatCounts(); + // Pangea# super.initState(); } @@ -73,9 +87,15 @@ class _SpaceViewState extends State { // Pangea# } - Future loadHierarchy([String? prevBatch]) async { + Future loadHierarchy([ + String? prevBatch, // #Pangea - if (widget.controller.activeSpaceId == null || loading) { + String? spaceId, + // Pangea# + ]) async { + // #Pangea + if ((widget.controller.activeSpaceId == null && spaceId == null) || + loading) { return GetSpaceHierarchyResponse( rooms: [], nextBatch: null, @@ -88,7 +108,10 @@ class _SpaceViewState extends State { }); // Pangea# - final activeSpaceId = widget.controller.activeSpaceId!; + // #Pangea + // final activeSpaceId = widget.controller.activeSpaceId!; + final activeSpaceId = (widget.controller.activeSpaceId ?? spaceId)!; + // Pangea# final client = Matrix.of(context).client; final activeSpace = client.getRoomById(activeSpaceId); @@ -121,6 +144,14 @@ class _SpaceViewState extends State { }); rethrow; } finally { + // #Pangea + if (activeSpace != null) { + await setChatCount( + activeSpace, + _lastResponse[activeSpaceId], + ); + } + // Pangea# setState(() { loading = false; }); @@ -173,7 +204,7 @@ class _SpaceViewState extends State { if (spaceChild.roomId == widget.controller.activeSpaceId) { // #Pangea // context.go('/rooms/${spaceChild.roomId}'); - context.push('/spaces/${spaceChild.roomId}'); + context.go('/rooms/${spaceChild.roomId}/details'); // Pangea# } else { widget.controller.setActiveSpace(spaceChild.roomId); @@ -206,6 +237,10 @@ class _SpaceViewState extends State { icon: Icons.send_outlined, ), if (spaceChild != null && + // #Pangea + room != null && + room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin && + // Pangea# (activeSpace?.canChangeStateEvent(EventTypes.spaceChild) ?? false)) SheetAction( key: SpaceChildContextAction.removeFromSpace, @@ -253,7 +288,10 @@ class _SpaceViewState extends State { // #Pangea // future: room!.leave, future: () async { - await room!.leave(); + if (room!.isUnread) { + await room.markUnread(false); + } + await room.leave(); if (Matrix.of(context).activeRoomId == room.id) { context.go('/rooms'); } @@ -386,6 +424,14 @@ class _SpaceViewState extends State { } // #Pangea + Future loadChatCounts() async { + for (final Room room in Matrix.of(context).client.rooms) { + if (room.isSpace && !chatCounts.containsKey(room.id)) { + await loadHierarchy(null, room.id); + } + } + } + Future refreshOnUpdate(SyncUpdate event) async { /* refresh on leave, invite, and space child update not join events, because there's already a listener on @@ -411,6 +457,90 @@ class _SpaceViewState extends State { } setState(() => refreshing = false); } + + bool includeSpaceChild(sc, matchingSpaceChildren) { + final bool isAnalyticsRoom = sc.roomType == PangeaRoomTypes.analytics; + final bool isMember = [Membership.join, Membership.invite] + .contains(Matrix.of(context).client.getRoomById(sc.roomId)?.membership); + final bool isSuggested = matchingSpaceChildren.any( + (matchingSpaceChild) => + matchingSpaceChild.roomId == sc.roomId && + matchingSpaceChild.suggested == true, + ); + return !isAnalyticsRoom && (isMember || isSuggested); + } + + List filterSpaceChildren( + Room space, + List spaceChildren, + ) { + final childIds = + spaceChildren.map((hierarchyMember) => hierarchyMember.roomId); + + final matchingSpaceChildren = space.spaceChildren + .where((spaceChild) => childIds.contains(spaceChild.roomId)) + .toList(); + + final filteredSpaceChildren = spaceChildren + .where( + (sc) => includeSpaceChild( + sc, + matchingSpaceChildren, + ), + ) + .toList(); + return filteredSpaceChildren; + } + + int sortSpaceChildren( + SpaceRoomsChunk a, + SpaceRoomsChunk b, + ) { + final bool aIsSpace = a.roomType == 'm.space'; + final bool bIsSpace = b.roomType == 'm.space'; + + if (aIsSpace && !bIsSpace) { + return -1; + } else if (!aIsSpace && bIsSpace) { + return 1; + } + return 0; + } + + Future setChatCount( + Room space, + GetSpaceHierarchyResponse? response, + ) async { + final Map updatedChatCounts = Map.from(chatCounts); + final List spaceChildren = response?.rooms ?? []; + final filteredChildren = filterSpaceChildren(space, spaceChildren) + .where((sc) => sc.roomId != space.id) + .toList(); + updatedChatCounts[space.id] = filteredChildren.length; + + await widget.controller.pangeaController.pStoreService.save( + _chatCountsKey, + updatedChatCounts, + local: true, + ); + } + + bool roomCountLoading(Room space) => + space.membership == Membership.join && !chatCounts.containsKey(space.id); + + Widget spaceSubtitle(Room space) { + if (roomCountLoading(space)) { + return const CircularProgressIndicator.adaptive(); + } + + return Text( + space.membership == Membership.join + ? L10n.of(context)!.numChats( + chatCounts[space.id].toString(), + ) + : L10n.of(context)!.youreInvited, + ); + } // Pangea# @override @@ -472,14 +602,8 @@ class _SpaceViewState extends State { // #Pangea subtitle: Row( children: [ - Text( - rootSpace.membership == Membership.join - ? L10n.of(context)!.numChats( - rootSpace.spaceChildren.length.toString(), - ) - : L10n.of(context)!.youreInvited, - ), - if (rootSpace.locked ?? false) + spaceSubtitle(rootSpace), + if (rootSpace.locked) const Padding( padding: EdgeInsets.only(left: 4.0), child: Icon( @@ -545,12 +669,25 @@ class _SpaceViewState extends State { titleSpacing: 0, title: ListTile( leading: BackButton( - onPressed: () => - widget.controller.setActiveSpace(parentSpace?.id), + // #Pangea + onPressed: () { + !FluffyThemes.isColumnMode(context) || + parentSpace?.id != null + ? widget.controller.setActiveSpace(parentSpace?.id) + : widget.controller.onDestinationSelected(0); + }, + // onPressed: () => + // widget.controller.setActiveSpace(parentSpace?.id), + // Pangea# ), title: Text( parentSpace == null - ? L10n.of(context)!.allSpaces + // #Pangea + // ? L10n.of(context)!.allSpaces + ? !FluffyThemes.isColumnMode(context) + ? L10n.of(context)!.allSpaces + : L10n.of(context)!.allChats + // Pangea# : parentSpace.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ), @@ -611,42 +748,9 @@ class _SpaceViewState extends State { final space = Matrix.of(context).client.getRoomById(activeSpaceId); if (space != null) { - final matchingSpaceChildren = space.spaceChildren - .where( - (spaceChild) => spaceChildren - .map((hierarchyMember) => hierarchyMember.roomId) - .contains(spaceChild.roomId), - ) - .toList(); - spaceChildren = spaceChildren - .where( - (spaceChild) => - matchingSpaceChildren.any( - (matchingSpaceChild) => - matchingSpaceChild.roomId == - spaceChild.roomId && - matchingSpaceChild.suggested == true, - ) || - [Membership.join, Membership.invite].contains( - Matrix.of(context) - .client - .getRoomById(spaceChild.roomId) - ?.membership, - ), - ) - .toList(); + spaceChildren = filterSpaceChildren(space, spaceChildren); } - spaceChildren.sort((a, b) { - final bool aIsSpace = a.roomType == 'm.space'; - final bool bIsSpace = b.roomType == 'm.space'; - - if (aIsSpace && !bIsSpace) { - return -1; - } else if (!aIsSpace && bIsSpace) { - return 1; - } - return 0; - }); + spaceChildren.sort(sortSpaceChildren); // Pangea# final canLoadMore = response.nextBatch != null; return SliverList( @@ -721,8 +825,8 @@ class _SpaceViewState extends State { ), // #Pangea // onTap: () => _onJoinSpaceChild(spaceChild), - onTap: () => context.push( - '/spaces/${spaceChild.roomId}', + onTap: () => context.go( + '/rooms/${spaceChild.roomId}/details', ), // Pangea# ), diff --git a/lib/pages/chat_list/start_chat_fab.dart b/lib/pages/chat_list/start_chat_fab.dart index 2bea8537e..f25374f4e 100644 --- a/lib/pages/chat_list/start_chat_fab.dart +++ b/lib/pages/chat_list/start_chat_fab.dart @@ -30,7 +30,9 @@ class StartChatFloatingActionButton extends StatelessWidget { void _onPressed(BuildContext context) async { //#Pangea if (controller.activeSpaceId != null) { - context.go('/rooms/newgroup/${controller.activeSpaceId ?? ''}'); + context.go( + '/rooms/newgroup${controller.activeSpaceId != null ? '?spaceId=${controller.activeSpaceId}' : ''}', + ); return; } //Pangea# @@ -44,7 +46,9 @@ class StartChatFloatingActionButton extends StatelessWidget { case ActiveFilter.groups: // #Pangea // context.go('/rooms/newgroup'); - context.go('/rooms/newgroup/${controller.activeSpaceId ?? ''}'); + context.go( + '/rooms/newgroup${controller.activeSpaceId != null ? '?spaceId=${controller.activeSpaceId}' : ''}', + ); // Pangea# break; case ActiveFilter.spaces: diff --git a/lib/pages/chat_list/utils/on_chat_tap.dart b/lib/pages/chat_list/utils/on_chat_tap.dart index d24af1fb4..d9f1c8191 100644 --- a/lib/pages/chat_list/utils/on_chat_tap.dart +++ b/lib/pages/chat_list/utils/on_chat_tap.dart @@ -1,15 +1,13 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/matrix.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 'package:fluffychat/pages/chat/send_file_dialog.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - void onChatTap(Room room, BuildContext context) async { if (room.membership == Membership.invite) { final inviterId = @@ -47,6 +45,11 @@ void onChatTap(Room room, BuildContext context) async { return; } if (inviteAction == InviteActions.decline) { + // #Pangea + if (room.isUnread) { + await room.markUnread(false); + } + // Pangea# await showFutureLoadingDialog( context: context, future: room.leave, diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 52e128ddc..5f2bd5027 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -157,7 +157,6 @@ class InvitationSelectionController extends State { //#Pangea // future: () => room.invite(id), future: () async { - await room.invite(id); if (mode == InvitationSelectionMode.admin) { await inviteTeacherAction(room, id); } @@ -175,7 +174,8 @@ class InvitationSelectionController extends State { // #Pangea Future inviteTeacherAction(Room room, String id) async { - room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); + await room.invite(id); + await room.setPower(id, ClassDefaultValues.powerLevelOfAdmin); if (room.isSpace) { for (final spaceChild in room.spaceChildren) { if (spaceChild.roomId == null) continue; diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 16d4e2cdb..2ada80dfa 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -17,7 +17,14 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; class NewGroup extends StatefulWidget { - const NewGroup({super.key}); + // #Pangea + final String? spaceId; + + const NewGroup({ + super.key, + this.spaceId, + }); + // Pangea# @override NewGroupController createState() => NewGroupController(); @@ -50,7 +57,7 @@ class NewGroupController extends State { void setVocab(List vocab) => setState(() => chatTopic.vocab = vocab); String? get activeSpaceId => - GoRouterState.of(context).pathParameters['spaceid']; + GoRouterState.of(context).uri.queryParameters['spaceId']; // Pangea# void setPublicGroup(bool b) => setState(() => publicGroup = b); diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 822b89214..523f3002e 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -57,6 +57,9 @@ class NewGroupView extends StatelessWidget { const SizedBox(width: 16), Expanded( child: TextField( + // #Pangea + maxLength: 32, + // Pangea# controller: controller.nameController, autocorrect: false, readOnly: controller.loading, @@ -91,7 +94,7 @@ class NewGroupView extends StatelessWidget { const Divider(height: 1), AddToSpaceToggles( key: controller.addToSpaceKey, - startOpen: false, + startOpen: true, activeSpaceId: controller.activeSpaceId, mode: AddToClassMode.chat, ), diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index be3e32dc5..a310769cc 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -216,7 +216,7 @@ class NewSpaceController extends State { ); MatrixState.pangeaController.classController .setActiveSpaceIdInChatListController(spaceId); - context.push('/spaces/$spaceId'); + context.go("/rooms/$spaceId/details"); return; } @@ -245,7 +245,7 @@ class NewSpaceController extends State { // context.pop(spaceId); MatrixState.pangeaController.classController .setActiveSpaceIdInChatListController(spaceId); - context.push('/spaces/$spaceId'); + context.go("/rooms/$spaceId/details"); // Pangea# } catch (e) { setState(() { diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index 71f887ef6..23f81b7ab 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -95,6 +95,9 @@ class NewSpaceView extends StatelessWidget { const SizedBox(width: 16), Expanded( child: TextField( + // #Pangea + maxLength: 32, + // Pangea# controller: controller.nameController, autocorrect: false, readOnly: controller.loading, @@ -138,7 +141,7 @@ class NewSpaceView extends StatelessWidget { if (!controller.newClassMode) AddToSpaceToggles( key: controller.addToSpaceKey, - startOpen: false, + startOpen: true, mode: !controller.newClassMode ? AddToClassMode.exchange : AddToClassMode.chat, diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 988ec3ff4..0e32fe478 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -42,6 +42,9 @@ class SettingsController extends State { cancelLabel: L10n.of(context)!.cancel, textFields: [ DialogTextField( + // #Pangea + maxLength: 32, + // Pangea# initialText: profile?.displayName ?? Matrix.of(context).client.userID!.localpart, ), diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 36766ea65..cc52f0ef0 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -181,14 +181,6 @@ class SettingsView extends StatelessWidget { trailing: const Icon(Icons.chevron_right_outlined), ), // #Pangea - ListTile( - leading: const Icon(Icons.account_circle_outlined), - title: Text(L10n.of(context)!.learningSettings), - onTap: () => context.go('/rooms/settings/learning'), - trailing: const Icon( - Icons.chevron_right_outlined, - ), - ), ListTile( leading: const Icon(Icons.account_circle_outlined), title: Text(L10n.of(context)!.subscriptionManagement), diff --git a/lib/pangea/choreographer/controllers/analytics_sender.dart b/lib/pangea/choreographer/controllers/analytics_sender.dart deleted file mode 100644 index fe4575f38..000000000 --- a/lib/pangea/choreographer/controllers/analytics_sender.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart'; - -class MlController { - final ITController controller; - MlController(this.controller); - - // sendPayloads(String message, String messageId) async { - // final MessageServiceModel serviceModel = MessageServiceModel( - // classId: controller.state!.classId, - // roomId: controller.state!.roomId, - // message: message.toString(), - // messageId: messageId.toString(), - // payloadIds: controller.state!.payLoadIds, - // userId: controller.state!.userId!, - // l1Lang: controller.state!.sourceLangCode, - // l2Lang: controller.state!.targetLangCode!, - // ); - // try { - // await MessageServiceRepo.sendPayloads(serviceModel); - // } catch (err) { - // debugPrint('$err in sendPayloads'); - // } - // } -} diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 156f36cc8..3a26676c6 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -211,7 +211,8 @@ class Choreographer { final CanSendStatus canSendStatus = pangeaController.subscriptionController.canSendStatus; - if (canSendStatus != CanSendStatus.subscribed) { + if (canSendStatus != CanSendStatus.subscribed || + (!igcEnabled && !itEnabled)) { return; } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 74d0b0159..83d678e98 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -15,7 +15,6 @@ import '../../models/it_response_model.dart'; import '../../models/it_step.dart'; import '../../models/system_choice_translation_model.dart'; import '../../repo/interactive_translation_repo.dart'; -import '../../repo/message_service.repo.dart'; import 'choreographer.dart'; class ITController { @@ -247,19 +246,19 @@ class ITController { ), ); - MessageServiceModel? messageServiceModelWithMessageId() => - usedInteractiveTranslation - ? MessageServiceModel( - classId: choreographer.classId, - roomId: choreographer.roomId, - message: choreographer.currentText, - messageId: null, - payloadIds: payLoadIds, - userId: choreographer.userId!, - l1Lang: sourceLangCode, - l2Lang: targetLangCode, - ) - : null; + // MessageServiceModel? messageServiceModelWithMessageId() => + // usedInteractiveTranslation + // ? MessageServiceModel( + // classId: choreographer.classId, + // roomId: choreographer.roomId, + // message: choreographer.currentText, + // messageId: null, + // payloadIds: payLoadIds, + // userId: choreographer.userId!, + // l1Lang: sourceLangCode, + // l2Lang: targetLangCode, + // ) + // : null; //maybe we store IT data in the same format? make a specific kind of match? void selectTranslation(int chosenIndex) { diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart index 7706d2194..c0a8e6093 100644 --- a/lib/pangea/controllers/class_controller.dart +++ b/lib/pangea/controllers/class_controller.dart @@ -120,21 +120,41 @@ class ClassController extends BaseController { if (classChunk == null) { ClassCodeUtil.messageSnack( - context, L10n.of(context)!.unableToFindClass); + context, + L10n.of(context)!.unableToFindClass, + ); return; } - if (Matrix.of(context) - .client - .rooms + if (_pangeaController.matrixState.client.rooms .any((room) => room.id == classChunk.roomId)) { setActiveSpaceIdInChatListController(classChunk.roomId); ClassCodeUtil.messageSnack(context, L10n.of(context)!.alreadyInClass); return; } await _pangeaController.matrixState.client.joinRoom(classChunk.roomId); - setActiveSpaceIdInChatListController(classChunk.roomId); + if (_pangeaController.matrixState.client.getRoomById(classChunk.roomId) == + null) { + await _pangeaController.matrixState.client.waitForRoomInSync( + classChunk.roomId, + join: true, + ); + } + + // add the user's analytics room to this joined space + // so their teachers can join them via the space hierarchy + final Room? joinedSpace = + _pangeaController.matrixState.client.getRoomById(classChunk.roomId); + + // ensure that the user has an analytics room for this space's language + await joinedSpace?.ensureAnalyticsRoomExists(); + + // when possible, add user's analytics room the to space they joined + await joinedSpace?.addAnalyticsRoomsToSpace(); + + // and invite the space's teachers to the user's analytics rooms + await joinedSpace?.inviteSpaceTeachersToAnalyticsRooms(); GoogleAnalytics.joinClass(classCode); return; } catch (err) { diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart new file mode 100644 index 000000000..0ff18b556 --- /dev/null +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -0,0 +1,138 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:http/http.dart' as http; + +import '../network/requests.dart'; + +class LanguageDetectionRequest { + /// The full text from which to detect the language. + String fullText; + + /// The base language of the user, if known. Including this is much preferred + /// and should return better results; however, it is not absolutely necessary. + /// This property is nullable to allow for situations where the languages are not set + /// at the time of the request. + String? userL1; + + /// The target language of the user. This is expected to be set for the request + /// but is nullable to handle edge cases where it might not be. + String? userL2; + + LanguageDetectionRequest({ + required this.fullText, + this.userL1 = "", + required this.userL2, + }); + + Map toJson() => { + 'full_text': fullText, + 'user_l1': userL1, + 'user_l2': userL2, + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is LanguageDetectionRequest && + other.fullText == fullText && + other.userL1 == userL1 && + other.userL2 == userL2; + } + + @override + int get hashCode => fullText.hashCode ^ userL1.hashCode ^ userL2.hashCode; +} + +class LanguageDetectionResponse { + List> detections; + String fullText; + + LanguageDetectionResponse({ + required this.detections, + required this.fullText, + }); + + factory LanguageDetectionResponse.fromJson(Map json) { + return LanguageDetectionResponse( + detections: List>.from(json['detections']), + fullText: json['full_text'], + ); + } + + Map toJson() { + return { + 'detections': detections, + 'full_text': fullText, + }; + } +} + +class _LanguageDetectionCacheItem { + Future data; + + _LanguageDetectionCacheItem({ + required this.data, + }); +} + +class LanguageDetectionController { + static final Map + _cache = {}; + late final PangeaController _pangeaController; + Timer? _cacheClearTimer; + + LanguageDetectionController(PangeaController pangeaController) { + _pangeaController = pangeaController; + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 15); // Adjust the duration as needed + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + Future get( + LanguageDetectionRequest params, + ) async { + if (_cache.containsKey(params)) { + return _cache[params]!.data; + } else { + final Future response = _fetchResponse( + await _pangeaController.userController.accessToken, + params, + ); + _cache[params] = _LanguageDetectionCacheItem(data: response); + return response; + } + } + + static Future _fetchResponse( + String accessToken, + LanguageDetectionRequest params, + ) async { + final Requests request = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final http.Response res = await request.post( + url: PApiUrls.languageDetection, + body: params.toJson(), + ); + + final Map json = jsonDecode(res.body); + return LanguageDetectionResponse.fromJson(json); + } +} diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 4b7c8cc96..808355b47 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -90,7 +90,7 @@ class MyAnalyticsController { } final Room analyticsRoom = await _pangeaController.matrixState.client .getMyAnalyticsRoom(langCode); - analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom(); + final List> saveFutures = []; for (final uses in aggregatedVocabUse.entries) { debugPrint("saving of type ${uses.value.first.constructType}"); diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index e2ddc6714..753a8c9e6 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/class_controller.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/language_controller.dart'; +import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/local_settings.dart'; import 'package:fluffychat/pangea/controllers/message_data_controller.dart'; @@ -17,6 +18,7 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -50,6 +52,7 @@ class PangeaController { late SubscriptionController subscriptionController; late TextToSpeechController textToSpeech; late SpeechToTextController speechToText; + late LanguageDetectionController languageDetection; ///store Services late PLocalStore pStoreService; @@ -97,6 +100,7 @@ class PangeaController { itFeedback = ITFeedbackController(this); textToSpeech = TextToSpeechController(this); speechToText = SpeechToTextController(this); + languageDetection = LanguageDetectionController(this); PAuthGaurd.pController = this; } @@ -272,6 +276,16 @@ class PangeaController { } Future setPangeaPushRules() async { + final List analyticsRooms = + matrixState.client.rooms.where((room) => room.isAnalyticsRoom).toList(); + + for (final Room room in analyticsRooms) { + final pushRule = room.pushRuleState; + if (pushRule != PushRuleState.dontNotify) { + await room.setPushRuleState(PushRuleState.dontNotify); + } + } + if (!(matrixState.client.globalPushRules?.override?.any( (element) => element.ruleId == PangeaEventTypes.textToSpeechRule, ) ?? diff --git a/lib/pangea/controllers/speech_to_text_controller.dart b/lib/pangea/controllers/speech_to_text_controller.dart index 6b302b101..8b61da79e 100644 --- a/lib/pangea/controllers/speech_to_text_controller.dart +++ b/lib/pangea/controllers/speech_to_text_controller.dart @@ -74,18 +74,21 @@ class SpeechToTextController { } debugPrint('Saving transcript as matrix event'); - requestModel.audioEvent?.room.sendPangeaEvent( - content: PangeaRepresentation( - langCode: response.langCode, - text: response.transcript.text, - originalSent: false, - originalWritten: false, - speechToText: response, - ).toJson(), - parentEventId: requestModel.audioEvent!.eventId, - type: PangeaEventTypes.representation, - ); - debugPrint('Transcript saved as matrix event'); + requestModel.audioEvent?.room + .sendPangeaEvent( + content: PangeaRepresentation( + langCode: response.langCode, + text: response.transcript.text, + originalSent: false, + originalWritten: false, + speechToText: response, + ).toJson(), + parentEventId: requestModel.audioEvent!.eventId, + type: PangeaEventTypes.representation, + ) + .then( + (_) => debugPrint('Transcript saved as matrix event'), + ); return Future.value(null); } diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart index c64861dc0..25948d23b 100644 --- a/lib/pangea/enum/message_mode_enum.dart +++ b/lib/pangea/enum/message_mode_enum.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; enum MessageMode { translation, definition, speechToText, textToSpeech } @@ -52,4 +53,17 @@ extension MessageModeExtension on MessageMode { .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode } } + + bool isValidMode(Event event) { + switch (this) { + case MessageMode.translation: + case MessageMode.textToSpeech: + case MessageMode.definition: + return event.messageType == MessageTypes.Text; + case MessageMode.speechToText: + return event.messageType == MessageTypes.Audio; + default: + return true; + } + } } diff --git a/lib/pangea/extensions/client_extension.dart b/lib/pangea/extensions/client_extension.dart index 6a6c3359d..b259d0c9e 100644 --- a/lib/pangea/extensions/client_extension.dart +++ b/lib/pangea/extensions/client_extension.dart @@ -88,7 +88,7 @@ extension PangeaClient on Client { for (final classRoom in classesAndExchangesImIn) { for (final teacher in await classRoom.teachers) { // If person requesting list of teachers is a teacher in another classroom, don't add them to the list - if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) { + if (!teachers.any((e) => e.id == teacher.id) && userID != teacher.id) { teachers.add(teacher); } } @@ -123,7 +123,7 @@ extension PangeaClient on Client { for (final room in rooms) { if (room.partial) await room.postLoad(); } - + final Room? analyticsRoom = analyticsRoomLocal(langCode); if (analyticsRoom != null) return analyticsRoom; @@ -168,14 +168,20 @@ extension PangeaClient on Client { // BotName.localBot, BotName.byEnvironment, ], - visibility: Visibility.private, - roomAliasName: "${userID!.localpart}_${langCode}_analytics", ); if (getRoomById(roomID) == null) { // Wait for room actually appears in sync await waitForRoomInSync(roomID, join: true); } + final Room? analyticsRoom = getRoomById(roomID); + + // add this analytics room to all spaces so teachers can join them + // via the space hierarchy + await analyticsRoom?.addAnalyticsRoomToSpaces(); + + // and invite all teachers to new analytics room + await analyticsRoom?.inviteTeachersToAnalyticsRoom(); return getRoomById(roomID)!; } @@ -245,4 +251,85 @@ extension PangeaClient on Client { editEvents.add(originalEvent); return editEvents.slice(1).map((e) => e.eventId).toList(); } + + // Get all my analytics rooms + List get allMyAnalyticsRooms => rooms + .where( + (e) => e.isAnalyticsRoomOfUser(userID!), + ) + .toList(); + + // migration function to change analytics rooms' vsibility to public + // so they will appear in the space hierarchy + Future updateAnalyticsRoomVisibility() async { + final List makePublicFutures = []; + for (final Room room in allMyAnalyticsRooms) { + final visability = await getRoomVisibilityOnDirectory(room.id); + if (visability != Visibility.public) { + await setRoomVisibilityOnDirectory( + room.id, + visibility: Visibility.public, + ); + } + } + await Future.wait(makePublicFutures); + } + + // Add all the users' analytics room to all the spaces the student studies in + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future addAnalyticsRoomsToAllSpaces() async { + final List addFutures = []; + for (final Room room in allMyAnalyticsRooms) { + addFutures.add(room.addAnalyticsRoomToSpaces()); + } + await Future.wait(addFutures); + } + + // Invite teachers to all my analytics room + // Handles case when students cannot add analytics room to space(s) + // So teacher is still able to get analytics data for this student + Future inviteAllTeachersToAllAnalyticsRooms() async { + final List inviteFutures = []; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + inviteFutures.add(analyticsRoom.inviteTeachersToAnalyticsRoom()); + } + await Future.wait(inviteFutures); + } + + // Join all analytics rooms in all spaces + // Allows teachers to join analytics rooms without being invited + Future joinAnalyticsRoomsInAllSpaces() async { + final List joinFutures = []; + for (final Room space in (await classesAndExchangesImTeaching)) { + joinFutures.add(space.joinAnalyticsRoomsInSpace()); + } + await Future.wait(joinFutures); + } + + // Join invited analytics rooms + // Checks for invites to any student analytics rooms + // Handles case of analytics rooms that can't be added to some space(s) + Future joinInvitedAnalyticsRooms() async { + for (final Room room in rooms) { + if (room.membership == Membership.invite && room.isAnalyticsRoom) { + try { + await room.join(); + } catch (err) { + debugPrint("Failed to join analytics room ${room.id}"); + } + } + } + } + + // helper function to join all relevant analytics rooms + // and set up those rooms to be joined by relevant teachers + Future migrateAnalyticsRooms() async { + await updateAnalyticsRoomVisibility(); + await addAnalyticsRoomsToAllSpaces(); + await inviteAllTeachersToAllAnalyticsRooms(); + await joinInvitedAnalyticsRooms(); + await joinAnalyticsRoomsInAllSpaces(); + } } diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 4e4f773db..febd17fa8 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:developer'; +import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; @@ -442,6 +443,7 @@ extension PangeaRoom on Room { /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event Future _createStudentAnalyticsEvent() async { try { + await postLoad(); if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { ErrorHandler.logError( m: "null powerLevels in createStudentAnalytics", @@ -453,7 +455,7 @@ extension PangeaRoom on Room { debugger(when: kDebugMode); throw Exception("null userId in createStudentAnalytics"); } - await postLoad(); + final String eventId = await client.setRoomStateWithKey( id, PangeaEventTypes.studentAnalyticsSummary, @@ -791,31 +793,6 @@ extension PangeaRoom on Room { } } - Future makeSureTeachersAreInvitedToAnalyticsRoom() async { - try { - if (!isAnalyticsRoom) { - throw Exception("not an analytics room"); - } - if (!participantListComplete) { - await requestParticipants(); - } - final toAdd = [ - ...getParticipants([Membership.invite, Membership.join]) - .map((e) => e.id), - BotName.byEnvironment, - ]; - for (final teacher in (await client.myTeachers)) { - if (!toAdd.contains(teacher.id)) { - debugPrint("inviting ${teacher.id} to analytics room"); - await invite(teacher.id); - } - } - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: stack); - } - } - /// update state event and return eventId Future updateStateEvent(Event stateEvent) { if (stateEvent.stateKey == null) { @@ -838,6 +815,9 @@ extension PangeaRoom on Room { ); return false; } + if (room != null && !room.isRoomAdmin) { + return false; + } if (!pangeaCanSendEvent(EventTypes.spaceChild) && !isRoomAdmin) { return false; } @@ -1059,4 +1039,223 @@ extension PangeaRoom on Room { getState(PangeaEventTypes.botOptions)?.content ?? {}, ); } + + // add 1 analytics room to 1 space + Future addAnalyticsRoomToSpace(Room analyticsRoom) async { + if (!isSpace) { + debugPrint("addAnalyticsRoomToSpace called on non-space room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "addAnalyticsRoomToSpace called on non-space room", + ), + ); + return Future.value(); + } + + if (spaceChildren.any((sc) => sc.roomId == analyticsRoom.id)) return; + if (canIAddSpaceChild(null)) { + try { + await setSpaceChild(analyticsRoom.id); + } catch (err) { + debugPrint( + "Failed to add analytics room ${analyticsRoom.id} for student to space $id", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: "Failed to add analytics room to space $id", + ), + ); + } + } + } + + // Add analytics room to all spaces the user is a student in (1 analytics room to all spaces) + // So teachers can join them via space hierarchy + // Will not always work, as there may be spaces where students don't have permission to add chats + // But allows teachers to join analytics rooms without being invited + Future addAnalyticsRoomToSpaces() async { + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("addAnalyticsRoomToSpaces called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "addAnalyticsRoomToSpaces called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; + await space.addAnalyticsRoomToSpace(this); + } + } + + // Add all analytics rooms to space + // Similar to addAnalyticsRoomToSpaces, but all analytics room to 1 space + Future addAnalyticsRoomsToSpace() async { + await postLoad(); + final List allMyAnalyticsRooms = client.allMyAnalyticsRooms; + for (final Room analyticsRoom in allMyAnalyticsRooms) { + await addAnalyticsRoomToSpace(analyticsRoom); + } + } + + // invite teachers of 1 space to 1 analytics room + Future inviteSpaceTeachersToAnalyticsRoom(Room analyticsRoom) async { + if (!isSpace) { + debugPrint( + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "inviteSpaceTeachersToAnalyticsRoom called on non-space room", + ), + ); + return; + } + if (!analyticsRoom.participantListComplete) { + await analyticsRoom.requestParticipants(); + } + final List participants = analyticsRoom.getParticipants(); + for (final User teacher in (await teachers)) { + if (!participants.any((p) => p.id == teacher.id)) { + try { + await analyticsRoom.invite(teacher.id); + } catch (err, s) { + debugPrint( + "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + ); + ErrorHandler.logError( + e: err, + m: "Failed to invite teacher ${teacher.id} to analytics room ${analyticsRoom.id}", + s: s, + ); + } + } + } + } + + // Invite all teachers to 1 analytics room + // Handles case when students cannot add analytics room to space + // So teacher is still able to get analytics data for this student + Future inviteTeachersToAnalyticsRoom() async { + if (client.userID == null) { + debugPrint("inviteTeachersToAnalyticsRoom called with null userId"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called with null userId", + ), + ); + return; + } + + if (!isAnalyticsRoomOfUser(client.userID!)) { + debugPrint("inviteTeachersToAnalyticsRoom called on non-analytics room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "inviteTeachersToAnalyticsRoom called on non-analytics room", + ), + ); + return; + } + + for (final Room space in (await client.classesAndExchangesImStudyingIn)) { + await space.inviteSpaceTeachersToAnalyticsRoom(this); + } + } + + // Invite teachers of 1 space to all users' analytics rooms + Future inviteSpaceTeachersToAnalyticsRooms() async { + for (final Room analyticsRoom in client.allMyAnalyticsRooms) { + await inviteSpaceTeachersToAnalyticsRoom(analyticsRoom); + } + } + + // Join analytics rooms in space + // Allows teachers to join analytics rooms without being invited + Future joinAnalyticsRoomsInSpace() async { + if (!isSpace) { + debugPrint("joinAnalyticsRoomsInSpace called on non-space room"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called on non-space room", + ), + ); + return; + } + + // added delay because without it power levels don't load and user is not + // recognized as admin + await Future.delayed(const Duration(milliseconds: 500)); + await postLoad(); + + if (!isRoomAdmin) { + debugPrint("joinAnalyticsRoomsInSpace called by non-admin"); + Sentry.addBreadcrumb( + Breadcrumb( + message: "joinAnalyticsRoomsInSpace called by non-admin", + ), + ); + return; + } + + final spaceHierarchy = await client.getSpaceHierarchy( + id, + maxDepth: 1, + ); + + final List analyticsRoomIds = spaceHierarchy.rooms + .where( + (r) => r.roomType == PangeaRoomTypes.analytics, + ) + .map((r) => r.roomId) + .toList(); + + for (final String roomID in analyticsRoomIds) { + try { + await joinSpaceChild(roomID); + } catch (err, s) { + debugPrint("Failed to join analytics room $roomID in space $id"); + ErrorHandler.logError( + e: err, + m: "Failed to join analytics room $roomID in space $id", + s: s, + ); + } + } + } + + Future joinSpaceChild(String roomID) async { + final Room? child = client.getRoomById(roomID); + if (child == null) { + await client.joinRoom( + roomID, + serverName: spaceChildren + .firstWhereOrNull((child) => child.roomId == roomID) + ?.via, + ); + if (client.getRoomById(roomID) == null) { + await client.waitForRoomInSync(roomID, join: true); + } + return; + } + + if (![Membership.invite, Membership.join].contains(child.membership)) { + final waitForRoom = client.waitForRoomInSync( + roomID, + join: true, + ); + await child.join(); + await waitForRoom; + } + } + + // check if analytics room exists for a given language code + // and if not, create it + Future ensureAnalyticsRoomExists() async { + await postLoad(); + if (firstLanguageSettings?.targetLanguage == null) return; + await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage); + } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 3f2f23616..91f19f6e7 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -80,18 +80,26 @@ class PangeaMessageEvent { return _latestEdit; } - bool showRichText(bool selected, bool highlighted) { + bool showRichText( + bool selected, { + bool highlighted = false, + bool isOverlay = false, + }) { if (!_isValidPangeaMessageEvent) { return false; } - // if (URLFinder.getMatches(event.body).isNotEmpty) { - // return false; - // } + if ([EventStatus.error, EventStatus.sending].contains(_event.status)) { return false; } - if (ownMessage && !selected && !highlighted) return false; + if (isOverlay) return true; + + // if ownMessage, don't show rich text if not selected or highlighted + // and don't show is the message is not an overlay + if (ownMessage && ((!selected && !highlighted) || !isOverlay)) { + return false; + } return true; } @@ -346,6 +354,19 @@ class PangeaMessageEvent { ), ); + _representations?.add( + RepresentationEvent( + timeline: timeline, + content: PangeaRepresentation( + langCode: response.langCode, + text: response.transcript.text, + originalSent: false, + originalWritten: false, + speechToText: response, + ), + ), + ); + return response; } @@ -564,17 +585,20 @@ class PangeaMessageEvent { return langCode ?? LanguageKeys.unknownLanguage; } - PangeaMatch? firstErrorStep(String lemma) { + List? errorSteps(String lemma) { final RepresentationEvent? repEvent = originalSent ?? originalWritten; if (repEvent?.choreo == null) return null; - final PangeaMatch? step = repEvent!.choreo!.choreoSteps - .firstWhereOrNull( - (element) => - element.acceptedOrIgnoredMatch?.match.shortMessage == lemma, + final List steps = repEvent!.choreo!.choreoSteps + .where( + (choreoStep) => + choreoStep.acceptedOrIgnoredMatch != null && + choreoStep.acceptedOrIgnoredMatch?.match.shortMessage == lemma, ) - ?.acceptedOrIgnoredMatch; - return step; + .map((element) => element.acceptedOrIgnoredMatch) + .cast() + .toList(); + return steps; } // List get activities => diff --git a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart index 0796f7f5d..e774dc4f2 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_choreo_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/repo/tokens_repo.dart'; import 'package:flutter/foundation.dart'; @@ -27,15 +26,12 @@ class RepresentationEvent { ChoreoRecord? _choreo; Timeline timeline; - SpeechToTextModel? _speechToTextResponse; - RepresentationEvent({ required this.timeline, Event? event, PangeaRepresentation? content, PangeaMessageTokens? tokens, ChoreoRecord? choreo, - SpeechToTextModel? speechToTextResponse, }) { if (event != null && event.type != PangeaEventTypes.representation) { throw Exception( @@ -46,7 +42,6 @@ class RepresentationEvent { _content = content; _tokens = tokens; _choreo = choreo; - _speechToTextResponse = speechToTextResponse; } Event? get event => _event; diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 19eaba750..a671256ab 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -60,7 +60,7 @@ class PangeaToken { static const String _lemmaKey = ModelKey.lemma; Map toJson() => { - _textKey: text, + _textKey: text.toJson(), _hasInfoKey: hasInfo, _lemmaKey: lemmas.map((e) => e.toJson()).toList(), }; diff --git a/lib/pangea/models/speech_to_text_models.dart b/lib/pangea/models/speech_to_text_models.dart index ad5fd96dd..3bcb4711c 100644 --- a/lib/pangea/models/speech_to_text_models.dart +++ b/lib/pangea/models/speech_to_text_models.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +const int THRESHOLD_FOR_GREEN = 80; + class SpeechToTextAudioConfigModel { final AudioEncodingEnum encoding; final int sampleRateHertz; @@ -93,13 +96,10 @@ class STTToken { ? Colors.white : Colors.black); } - if (confidence! > 80) { - return const Color.fromARGB(255, 0, 152, 0); + if (confidence! > THRESHOLD_FOR_GREEN) { + return AppConfig.success; } - if (confidence! > 50) { - return const Color.fromARGB(255, 184, 142, 43); - } - return Colors.red; + return AppConfig.warning; } factory STTToken.fromJson(Map json) { @@ -117,7 +117,7 @@ class STTToken { } Map toJson() => { - "token": token, + "token": token.toJson(), "start_time": startTime?.inMilliseconds, "end_time": endTime?.inMilliseconds, "confidence": confidence, @@ -150,14 +150,19 @@ class Transcript { final int confidence; final List sttTokens; final String langCode; + final int? wordsPerHr; Transcript({ required this.text, required this.confidence, required this.sttTokens, required this.langCode, + required this.wordsPerHr, }); + /// Returns the number of words per minute rounded to one decimal place. + double? get wordsPerMinute => wordsPerHr != null ? wordsPerHr! / 60 : null; + factory Transcript.fromJson(Map json) => Transcript( text: json['transcript'], confidence: json['confidence'] <= 100 @@ -167,6 +172,7 @@ class Transcript { .map((e) => STTToken.fromJson(e)) .toList(), langCode: json['lang_code'], + wordsPerHr: json['words_per_hr'], ); Map toJson() => { @@ -174,7 +180,15 @@ class Transcript { "confidence": confidence, "stt_tokens": sttTokens.map((e) => e.toJson()).toList(), "lang_code": langCode, + "words_per_hr": wordsPerHr, }; + + Color color(BuildContext context) { + if (confidence > THRESHOLD_FOR_GREEN) { + return AppConfig.success; + } + return AppConfig.warning; + } } class SpeechToTextResult { diff --git a/lib/pangea/models/word_data_model.dart b/lib/pangea/models/word_data_model.dart index 838240725..c4c059241 100644 --- a/lib/pangea/models/word_data_model.dart +++ b/lib/pangea/models/word_data_model.dart @@ -1,9 +1,8 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; - import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:flutter/foundation.dart'; class WordData { final String word; @@ -102,10 +101,11 @@ class WordData { }) => word == w && userL1 == l1 && userL2 == l2 && fullText == f; - String formattedPartOfSpeech(LanguageType languageType) { + String? formattedPartOfSpeech(LanguageType languageType) { final String pos = languageType == LanguageType.base ? basePartOfSpeech : targetPartOfSpeech; + if (pos.isEmpty) return null; return pos[0].toUpperCase() + pos.substring(1); } diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 16d8bcd23..ff0404947 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -24,13 +24,12 @@ class PApiUrls { /// ---------------------- Conversation Partner ------------------------- static String searchUserProfiles = "/account/search"; - ///-------------------------------- Deprecated analytics -------------------- - static String classAnalytics = "${Environment.choreoApi}/class_analytics"; - static String messageService = "/message_service"; - ///-------------------------------- choreo -------------------------- static String igc = "${Environment.choreoApi}/grammar"; + static String languageDetection = + "${Environment.choreoApi}/language_detection"; + static String igcLite = "${Environment.choreoApi}/grammar_lite"; static String spanDetails = "${Environment.choreoApi}/span_details"; diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index ae81e957c..ab35b2610 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -52,7 +52,11 @@ class AnalyticsListTileState extends State { child: Opacity( opacity: widget.enabled ? 1 : 0.5, child: Tooltip( - message: widget.enabled ? "" : L10n.of(context)!.joinToView, + message: widget.enabled + ? "" + : widget.type == AnalyticsEntryType.room + ? L10n.of(context)!.joinToView + : L10n.of(context)!.studentAnalyticsNotAvailable, child: ListTile( leading: widget.type == AnalyticsEntryType.privateChats ? CircleAvatar( @@ -101,18 +105,19 @@ class AnalyticsListTileState extends State { : null, selected: widget.selected, enabled: widget.enabled, - onTap: () => - (room?.isSpace ?? false) && widget.allowNavigateOnSelect - ? context.go( - '/rooms/analytics/${room!.id}', - ) - : widget.onTap( - AnalyticsSelected( - widget.id, - widget.type, - widget.displayName, - ), + onTap: () { + (room?.isSpace ?? false) && widget.allowNavigateOnSelect + ? context.go( + '/rooms/analytics/${room!.id}', + ) + : widget.onTap( + AnalyticsSelected( + widget.id, + widget.type, + widget.displayName, ), + ); + }, trailing: (room?.isSpace ?? false) && widget.type != AnalyticsEntryType.privateChats && widget.allowNavigateOnSelect diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 7182bc6ab..634a39980 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/extensions/client_extension.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart'; import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart'; import 'package:flutter/material.dart'; @@ -142,14 +143,29 @@ class BaseAnalyticsController extends State { } bool enableSelection(AnalyticsSelected? selectedParam) { - return selectedView == BarChartViewSelection.grammar && - selectedParam?.type == AnalyticsEntryType.room - ? Matrix.of(context) + if (selectedView == BarChartViewSelection.grammar) { + if (selectedParam?.type == AnalyticsEntryType.room) { + return Matrix.of(context) .client .getRoomById(selectedParam!.id) ?.membership == - Membership.join - : true; + Membership.join; + } + + if (selectedParam?.type == AnalyticsEntryType.student) { + final String? langCode = + pangeaController.languageController.activeL2Code( + roomID: widget.defaultSelected.id, + ); + if (langCode == null) return false; + return Matrix.of(context).client.analyticsRoomLocal( + langCode, + selectedParam?.id, + ) != + null; + } + } + return true; } @override diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 1f331d2d5..86f179829 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -246,6 +246,15 @@ class BaseAnalyticsView extends StatelessWidget { .widget .tabs[1] .allowNavigateOnSelect, + enabled: + controller.enableSelection( + AnalyticsSelected( + item.id, + controller + .widget.tabs[1].type, + "", + ), + ), ), ) .toList(), diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart index 0316d02cd..877ae1788 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; @@ -103,7 +104,11 @@ class ClassAnalyticsV2Controller extends State { students = classRoom!.students; chats = response.rooms - .where((room) => room.roomId != classRoom!.id) + .where( + (room) => + room.roomId != classRoom!.id && + room.roomType != PangeaRoomTypes.analytics, + ) .toList(); chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1); } diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart index 0c0550172..88c15bdf5 100644 --- a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -20,7 +21,12 @@ class ClassAnalyticsView extends StatelessWidget { .map( (room) => TabItem( avatar: room.avatarUrl, - displayName: room.name ?? "", + displayName: room.name ?? + Matrix.of(context) + .client + .getRoomById(room.roomId) + ?.getLocalizedDisplayname() ?? + "", id: room.roomId, ), ) @@ -33,7 +39,7 @@ class ClassAnalyticsView extends StatelessWidget { .map( (s) => TabItem( avatar: s.avatarUrl, - displayName: s.displayName ?? "unknown", + displayName: s.calcDisplayname(), id: s.id, ), ) diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index dd6c26618..ffcf0e79a 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_ev import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -54,7 +55,7 @@ class ConstructListState extends State { selected: widget.selected, forceUpdate: true, ) - .then((_) => setState(() => initialized = true)); + .whenComplete(() => setState(() => initialized = true)); } @override @@ -160,11 +161,11 @@ class ConstructListViewState extends State { stateSub?.cancel(); } - @override - void didUpdateWidget(ConstructListView oldWidget) { - super.didUpdateWidget(oldWidget); - fetchUses(); - } + // @override + // void didUpdateWidget(ConstructListView oldWidget) { + // super.didUpdateWidget(oldWidget); + // fetchUses(); + // } int get lemmaIndex => constructs?.indexWhere( @@ -215,19 +216,29 @@ class ConstructListViewState extends State { } setState(() => fetchingUses = true); - final List uses = currentConstruct!.content.uses; - _msgEvents.clear(); + try { + final List uses = currentConstruct!.content.uses; + _msgEvents.clear(); - for (final OneConstructUse use in uses) { - final PangeaMessageEvent? msgEvent = await getMessageEvent(use); - final RepresentationEvent? repEvent = - msgEvent?.originalSent ?? msgEvent?.originalWritten; - if (repEvent?.choreo == null) { - continue; + for (final OneConstructUse use in uses) { + final PangeaMessageEvent? msgEvent = await getMessageEvent(use); + final RepresentationEvent? repEvent = + msgEvent?.originalSent ?? msgEvent?.originalWritten; + if (repEvent?.choreo == null) { + continue; + } + _msgEvents.add(msgEvent!); } - _msgEvents.add(msgEvent!); + setState(() => fetchingUses = false); + } catch (err, s) { + setState(() => fetchingUses = false); + debugPrint("Error fetching uses: $err"); + ErrorHandler.logError( + e: err, + s: s, + m: "Failed to fetch uses for current construct ${currentConstruct?.content.lemma}", + ); } - setState(() => fetchingUses = false); } List? get constructs => @@ -237,6 +248,38 @@ class ConstructListViewState extends State { (element) => element.content.lemma == widget.controller.currentLemma, ); + // given the current lemma and list of message events, return a list of + // MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch + // this is because some message events may have has more than one PangeaMatch of a + // given lemma type. + List getMessageEventMatches() { + if (widget.controller.currentLemma == null) return []; + final List allMsgErrorSteps = []; + + for (final msgEvent in _msgEvents) { + if (allMsgErrorSteps.any( + (element) => element.msgEvent.eventId == msgEvent.eventId, + )) { + continue; + } + // get all the pangea matches in that message which have that lemma + final List? msgErrorSteps = msgEvent.errorSteps( + widget.controller.currentLemma!, + ); + if (msgErrorSteps == null) continue; + + allMsgErrorSteps.addAll( + msgErrorSteps.map( + (errorStep) => MessageEventMatch( + msgEvent: msgEvent, + lemmaMatch: errorStep, + ), + ), + ); + } + return allMsgErrorSteps; + } + @override Widget build(BuildContext context) { if (!widget.init || fetchingUses) { @@ -251,6 +294,8 @@ class ConstructListViewState extends State { ); } + final msgEventMatches = getMessageEventMatches(); + return widget.controller.currentLemma == null ? Expanded( child: ListView.builder( @@ -278,23 +323,22 @@ class ConstructListViewState extends State { children: [ if (constructs![lemmaIndex].content.uses.length > _msgEvents.length) - const Center( + Center( child: Padding( - padding: EdgeInsets.all(8.0), - child: Text( - "Some data may be missing from rooms in which you are not a member.", - ), + padding: const EdgeInsets.all(8.0), + child: Text(L10n.of(context)!.roomDataMissing), ), ), Expanded( child: ListView.separated( separatorBuilder: (context, index) => const Divider(height: 1), - itemCount: _msgEvents.length, + itemCount: msgEventMatches.length, itemBuilder: (context, index) { return ConstructMessage( - msgEvent: _msgEvents[index], + msgEvent: msgEventMatches[index].msgEvent, lemma: widget.controller.currentLemma!, + errorMessage: msgEventMatches[index].lemmaMatch, ); }, ), @@ -307,21 +351,18 @@ class ConstructListViewState extends State { class ConstructMessage extends StatelessWidget { final PangeaMessageEvent msgEvent; + final PangeaMatch errorMessage; final String lemma; const ConstructMessage({ super.key, required this.msgEvent, + required this.errorMessage, required this.lemma, }); @override Widget build(BuildContext context) { - final PangeaMatch? errorMessage = msgEvent.firstErrorStep(lemma); - if (errorMessage == null) { - return const SizedBox.shrink(); - } - final String? chosen = errorMessage.match.choices ?.firstWhereOrNull( (element) => element.selected == true, @@ -479,6 +520,14 @@ 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; + return Padding( padding: const EdgeInsets.fromLTRB(10, 0, 30, 0), child: Column( @@ -487,9 +536,19 @@ class ConstructMessageMetadata extends StatelessWidget { msgEvent.event.originServerTs.localizedTime(context), style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor), ), - Text(msgEvent.event.room.name), + Text(roomName), ], ), ); } } + +class MessageEventMatch { + final PangeaMessageEvent msgEvent; + final PangeaMatch lemmaMatch; + + MessageEventMatch({ + required this.msgEvent, + required this.lemmaMatch, + }); +} diff --git a/lib/pangea/pages/class_settings/class_settings_page.dart b/lib/pangea/pages/class_settings/class_settings_page.dart deleted file mode 100644 index d0c2d385d..000000000 --- a/lib/pangea/pages/class_settings/class_settings_page.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/widgets/layouts/empty_page.dart'; -import '../../../widgets/matrix.dart'; -import '../../utils/error_handler.dart'; -import '../../utils/set_class_name.dart'; -import '../../widgets/space/class_settings.dart'; -import 'class_settings_view.dart'; -import 'p_class_widgets/room_rules_editor.dart'; - -class ClassSettingsPage extends StatefulWidget { - const ClassSettingsPage({super.key}); - - @override - State createState() => ClassSettingsController(); -} - -class ClassSettingsController extends State { - PangeaController pangeaController = MatrixState.pangeaController; - - final GlobalKey rulesEditorKey = GlobalKey(); - final GlobalKey classSettingsKey = - GlobalKey(); - - Room? room; - - String? get roomId => GoRouterState.of(context).pathParameters['roomid']; - - Future handleSave(BuildContext context) async { - if (classSettingsKey.currentState!.sameLanguages) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.noIdenticalLanguages), - ), - ); - return; - } - if (rulesEditorKey.currentState != null) { - await rulesEditorKey.currentState?.setRoomRules(roomId!); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError(m: "Null rules editor state"); - } - if (classSettingsKey.currentState != null) { - await classSettingsKey.currentState?.setClassSettings( - roomId!, - ); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError(m: "Null class settings state"); - } - } - - void goback(BuildContext context) { - context.push("/spaces/$roomId"); - } - - String get className => - Matrix.of(context).client.getRoomById(roomId!)?.name ?? ''; - - @override - void initState() { - // TODO: implement initState - super.initState(); - - Future.delayed(Duration.zero, () { - room = Matrix.of(context).client.getRoomById(roomId!); - if (room == null) { - debugger(when: kDebugMode); - context.pop(); - } - setState(() {}); - }); - } - //PTODO - show loading widget - - void setDisplaynameAction() => setClassDisplayname(context, roomId); - - bool showEditNameIcon = false; - void hoverEditNameIcon(bool hovering) => - setState(() => showEditNameIcon = !showEditNameIcon); - - @override - Widget build(BuildContext context) => room != null - ? ClassSettingsPageView(controller: this) - : const EmptyPage(); -} diff --git a/lib/pangea/pages/class_settings/class_settings_view.dart b/lib/pangea/pages/class_settings/class_settings_view.dart deleted file mode 100644 index f6fa434e9..000000000 --- a/lib/pangea/pages/class_settings/class_settings_view.dart +++ /dev/null @@ -1,85 +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:go_router/go_router.dart'; - -import 'package:fluffychat/pangea/pages/class_settings/class_settings_page.dart'; -import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; -import 'package:fluffychat/pangea/widgets/space/class_settings.dart'; -import '../../../widgets/layouts/max_width_body.dart'; - -class ClassSettingsPageView extends StatelessWidget { - final ClassSettingsController controller; - const ClassSettingsPageView({super.key, required this.controller}); - - @override - Widget build(BuildContext context) { - debugPrint("in class settings page with roomId ${controller.roomId}"); - // PTODO-Lala - make the page scrollable anywhere, not just in the area of the elements - // so like, the user should be able scroll using the mouse wheel from anywhere within this view - // currently, your cursor needs be horizontally within the tiles in order to scroll - return Scaffold( - appBar: AppBar( - leading: GoRouterState.of(context).path?.startsWith('/spaces/') ?? false - ? null - : IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () => controller.goback(context), - ), - centerTitle: true, - title: Text(L10n.of(context)!.classSettings), - ), - body: ListView( - children: [ - MaxWidthBody( - child: ListTile( - title: Center( - child: TextButton.icon( - onPressed: controller.setDisplaynameAction, - onHover: controller.hoverEditNameIcon, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 25), - ), - label: Visibility( - visible: controller.showEditNameIcon, - child: Icon( - Icons.edit, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - icon: Text( - controller.className, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), - MaxWidthBody( - child: Column( - children: [ - ClassSettings( - roomId: controller.roomId, - startOpen: true, - ), - RoomRulesEditor(roomId: controller.roomId), - ], - ), - ), - ], - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => showFutureLoadingDialog( - context: context, - future: () => controller.handleSave(context), - ), - label: Text(L10n.of(context)!.saveChanges), - icon: const Icon(Icons.save_outlined), - ), - ); - } -} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart deleted file mode 100644 index 3915d0ca0..000000000 --- a/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - -class ClassSettingsButton extends StatelessWidget { - const ClassSettingsButton({super.key}); - - // final PangeaController _pangeaController = MatrixState.pangeaController; - - @override - Widget build(BuildContext context) { - // final roomId = GoRouterState.of(context).pathParameters['roomid']; - - final iconColor = Theme.of(context).textTheme.bodyLarge!.color; - return Column( - children: [ - ListTile( - // enabled: roomId != null && - // _pangeaController.classController - // .getClassModelBySpaceIdLocal(roomId) != - // null, - title: Text( - L10n.of(context)!.classSettings, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text(L10n.of(context)!.classSettingsDesc), - leading: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.settings_outlined), - ), - onTap: () => context.go('/class_settings'), - ), - ], - ); - } -} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart b/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart deleted file mode 100644 index 7e3fc3af6..000000000 --- a/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart +++ /dev/null @@ -1,149 +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:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/utils/delete_room.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class DeleteSpaceTile extends StatelessWidget { - final Room room; - - const DeleteSpaceTile({ - super.key, - required this.room, - }); - - @override - Widget build(BuildContext context) { - bool classNameMatch = true; - final textController = TextEditingController(); - Future deleteSpace() async { - final Client client = Matrix.of(context).client; - final GetSpaceHierarchyResponse spaceHierarchy = - await client.getSpaceHierarchy(room.id); - - if (spaceHierarchy.rooms.isNotEmpty) { - final List spaceChats = spaceHierarchy.rooms - .where((c) => c.roomId != room.id) - .map((e) => Matrix.of(context).client.getRoomById(e.roomId)) - .where((c) => c != null && !c.isSpace && !c.isDirectChat) - .cast() - .toList(); - - await Future.wait( - spaceChats.map((c) => deleteRoom(c.id, client)), - ); - } - deleteRoom(room.id, client); - context.go('/rooms'); - return; - } - - Future deleteChat() { - context.go('/rooms'); - return deleteRoom(room.id, Matrix.of(context).client); - } - - Future deleteChatAction() async { - showDialog( - context: context, - useRootNavigator: false, - builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - room.isSpace - ? L10n.of(context)!.areYouSureDeleteClass - : L10n.of(context)!.areYouSureDeleteGroup, - style: const TextStyle( - fontSize: 20, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 5), - Text( - L10n.of(context)!.cannotBeReversed, - style: const TextStyle( - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - if (room.isSpace) - Text( - L10n.of(context)!.enterDeletedClassName, - style: const TextStyle( - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - ], - ), - content: room.isSpace - ? TextField( - autofocus: true, - controller: textController, - decoration: InputDecoration( - hintText: room.name, - errorText: !classNameMatch - ? L10n.of(context)!.incorrectClassName - : null, - ), - ) - : null, - actions: [ - TextButton( - child: Text(L10n.of(context)!.ok), - onPressed: () async { - if (room.isSpace) { - setState(() { - classNameMatch = textController.text == room.name; - }); - if (classNameMatch) { - Navigator.of(context).pop(); - await showFutureLoadingDialog( - context: context, - future: () => deleteSpace(), - ); - } - } else { - await showFutureLoadingDialog( - context: context, - future: () => deleteChat(), - ); - } - }, - ), - TextButton( - child: Text(L10n.of(context)!.cancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - }, - ); - } - - return ListTile( - trailing: const Icon(Icons.delete_outlined), - title: Text( - room.isSpace - ? L10n.of(context)!.deleteSpace - : L10n.of(context)!.deleteGroup, - style: const TextStyle(color: Colors.red), - ), - onTap: () => deleteChatAction(), - ); - } -} diff --git a/lib/pangea/pages/new_class/new_class.dart b/lib/pangea/pages/new_class/new_class.dart deleted file mode 100644 index 55b7d083d..000000000 --- a/lib/pangea/pages/new_class/new_class.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:developer'; - -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:go_router/go_router.dart'; -import 'package:matrix/matrix.dart' as sdk; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/pages/new_class/new_class_view.dart'; -import 'package:fluffychat/pangea/utils/class_code.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../../controllers/pangea_controller.dart'; -import '../../widgets/space/class_settings.dart'; -import '../class_settings/p_class_widgets/room_rules_editor.dart'; - -class NewClass extends StatefulWidget { - const NewClass({super.key}); - - @override - NewClassController createState() => NewClassController(); -} - -class NewClassController extends State { - TextEditingController controller = TextEditingController(); - - final PangeaController pangeaController = MatrixState.pangeaController; - final GlobalKey rulesEditorKey = GlobalKey(); - final GlobalKey classSettingsKey = - GlobalKey(); - - void submitAction([_]) async { - //TODO: validate that object is complete - final matrix = Matrix.of(context); - if (controller.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.classNameRequired), - ), - ); - return; - } - if (classSettingsKey.currentState == null) { - debugger(when: kDebugMode); - } - if (classSettingsKey.currentState!.sameLanguages) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.noIdenticalLanguages), - ), - ); - return; - } - - final roomID = await showFutureLoadingDialog( - context: context, - future: () async { - final String roomID = await matrix.client.createRoom( - //PTODO - investigate effects of changing visibility from public - preset: sdk.CreateRoomPreset.publicChat, - creationContent: { - 'type': RoomCreationTypes.mSpace, - }, - visibility: sdk.Visibility.public, - // roomAliasName: controller.text.isNotEmpty - // ? "${matrix.client.userID!.localpart}-${controller.text.trim().toLowerCase().replaceAll(' ', '_')}" - // : null, - roomAliasName: ClassCodeUtil.generateClassCode(), - name: controller.text.isNotEmpty ? controller.text : null, - ); - - if (rulesEditorKey.currentState != null) { - await rulesEditorKey.currentState!.setRoomRules(roomID); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError(m: "Null rules editor state"); - } - if (classSettingsKey.currentState != null) { - await classSettingsKey.currentState!.setClassSettings( - roomID, - ); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError(m: "Null class settings state"); - } - return roomID; - }, - onError: (e) { - debugger(when: kDebugMode); - return e; - }, - ); - - if (roomID.error == null && roomID.result is String) { - pangeaController.classController.setActiveSpaceIdInChatListController( - roomID.result!, - ); - context.push('/spaces/${roomID.result!}'); - } else { - debugger(when: kDebugMode); - ErrorHandler.logError(e: roomID.error, s: StackTrace.current); - } - } - - @override - void initState() { - // TODO: implement initState - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => NewSpaceView(this); -} diff --git a/lib/pangea/pages/new_class/new_class_view.dart b/lib/pangea/pages/new_class/new_class_view.dart deleted file mode 100644 index 13796e2d8..000000000 --- a/lib/pangea/pages/new_class/new_class_view.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/pages/new_class/new_class.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import '../../widgets/space/class_settings.dart'; -import '../class_settings/p_class_widgets/room_rules_editor.dart'; - -class NewSpaceView extends StatelessWidget { - // #Pangea - // final NewSpaceController controller; - final NewClassController controller; - // Pangea# - - const NewSpaceView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - // #Pangea - centerTitle: true, - // Pangea# - title: Text(L10n.of(context)!.createNewClass), - ), - body: MaxWidthBody( - // #Pangea - child: ListView( - // child: Column( - // mainAxisSize: MainAxisSize.min, - // #Pangea - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - // #Pangea - maxLength: ClassDefaultValues.maxClassName, - maxLengthEnforcement: MaxLengthEnforcement.enforced, - // #Pangea - controller: controller.controller, - autofocus: true, - autocorrect: false, - textInputAction: TextInputAction.go, - onSubmitted: controller.submitAction, - decoration: InputDecoration( - labelText: L10n.of(context)!.spaceName, - prefixIcon: const Icon(Icons.people_outlined), - hintText: L10n.of(context)!.enterASpacepName, - ), - ), - ), - // #Pangea - ClassSettings( - key: controller.classSettingsKey, - roomId: null, - startOpen: true, - ), - RoomRulesEditor( - key: controller.rulesEditorKey, - roomId: null, - ), - const SizedBox(height: 45), - // SwitchListTile.adaptive( - // title: Text(L10n.of(context)!.spaceIsPublic), - // value: controller.publicGroup, - // onChanged: controller.setPublicGroup, - // ), - // ListTile( - // trailing: const Padding( - // padding: EdgeInsets.symmetric(horizontal: 16.0), - // child: Icon(Icons.info_outlined), - // ), - // subtitle: Text(L10n.of(context)!.newSpaceDescription), - // ), - // #Pangea - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: controller.submitAction, - child: const Icon(Icons.arrow_forward_outlined), - ), - ); - } -} diff --git a/lib/pangea/pages/settings_learning/settings_learning.dart b/lib/pangea/pages/settings_learning/settings_learning.dart index f699fc7b5..c560fef57 100644 --- a/lib/pangea/pages/settings_learning/settings_learning.dart +++ b/lib/pangea/pages/settings_learning/settings_learning.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class SettingsLearning extends StatefulWidget { const SettingsLearning({super.key}); @@ -32,6 +32,11 @@ class SettingsLearningController extends State { }); } + Future changeLanguage() async { + await pLanguageDialog(context, () {}); + setState(() {}); + } + @override void dispose() { super.dispose(); diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart index 125b2ac99..6c3a87f00 100644 --- a/lib/pangea/pages/settings_learning/settings_learning_view.dart +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -31,7 +31,7 @@ class SettingsLearningView extends StatelessWidget { withScrolling: true, child: Column( children: [ - LanguageTile(), + LanguageTile(controller), CountryPickerTile(), const SizedBox(height: 8), const Divider(height: 1), @@ -65,7 +65,7 @@ class SettingsLearningView extends StatelessWidget { defaultValue: controller.pangeaController.pStoreService.read( PLocalKey.autoPlayMessages, ) ?? - true, + false, title: L10n.of(context)!.autoPlayTitle, subtitle: L10n.of(context)!.autoPlayDesc, pStoreKey: PLocalKey.autoPlayMessages, diff --git a/lib/pangea/repo/message_service.repo.dart b/lib/pangea/repo/message_service.repo.dart deleted file mode 100644 index ce51a3802..000000000 --- a/lib/pangea/repo/message_service.repo.dart +++ /dev/null @@ -1,55 +0,0 @@ -import '../config/environment.dart'; -import '../network/requests.dart'; -import '../network/urls.dart'; - -class MessageServiceRepo { - static Future sendPayloads( - MessageServiceModel serviceModel, - String messageId, - ) async { - final Requests req = Requests( - baseUrl: Environment.choreoApi, - choreoApiKey: Environment.choreoApiKey, - ); - - final json = serviceModel.toJson(); - json["msg_id"] = messageId; - - await req.post(url: PApiUrls.messageService, body: json); - } -} - -class MessageServiceModel { - List payloadIds; - String? messageId; - String message; - String userId; - String roomId; - String? classId; - String? l1Lang; - String l2Lang; - - MessageServiceModel({ - required this.payloadIds, - required this.messageId, - required this.message, - required this.userId, - required this.roomId, - required this.classId, - required this.l1Lang, - required this.l2Lang, - }); - - toJson() { - return { - 'payload_ids': payloadIds, - 'msg_id': messageId, - 'message': message, - 'user_id': userId, - 'room_id': roomId, - 'class_id': classId, - 'l1_lang': l1Lang, - 'l2_lang': l2Lang, - }; - } -} diff --git a/lib/pangea/utils/add_to_space.dart b/lib/pangea/utils/add_to_space.dart index 92458856c..f35b8f308 100644 --- a/lib/pangea/utils/add_to_space.dart +++ b/lib/pangea/utils/add_to_space.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; bool canAddToSpace(Room space, PangeaController pangeaController) { final bool pangeaPermission = @@ -27,8 +25,9 @@ Future pangeaAddToSpace( Room space, List selectedRoomIds, BuildContext context, - PangeaController pangeaController, -) async { + PangeaController pangeaController, { + bool suggested = true, +}) async { if (!canAddToSpace(space, pangeaController)) { throw L10n.of(context)!.noAddToSpacePermissions; } @@ -37,6 +36,6 @@ Future pangeaAddToSpace( if (room != null && chatIsInSpace(room, space)) { throw L10n.of(context)!.alreadyInSpace; } - await space.setSpaceChild(roomId); + await space.setSpaceChild(roomId, suggested: suggested); } } diff --git a/lib/pangea/utils/archive_space.dart b/lib/pangea/utils/archive_space.dart index 72f10fae4..ac83980fb 100644 --- a/lib/pangea/utils/archive_space.dart +++ b/lib/pangea/utils/archive_space.dart @@ -1,7 +1,6 @@ -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:matrix/matrix.dart'; Future archiveSpace(Room? space, Client client) async { if (space == null) { @@ -14,6 +13,9 @@ Future archiveSpace(Room? space, Client client) async { final List children = await space.getChildRooms(); for (final Room child in children) { + if (child.isUnread) { + await child.markUnread(false); + } await child.leave(); } await space.leave(); diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart index ef0865634..01d99bda2 100644 --- a/lib/pangea/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -21,7 +21,7 @@ void chatListHandleSpaceTap( controller.setActiveSpace(space.id); if (FluffyThemes.isColumnMode(context)) { - context.push('/spaces/${space.id}'); + context.go('/rooms/${space.id}/details'); } else if (controller.activeChat != null && !space.isFirstOrSecondChild(controller.activeChat!)) { context.go("/rooms"); @@ -65,6 +65,9 @@ void chatListHandleSpaceTap( context: context, future: () async { await space.join(); + if (space.isSpace) { + await space.joinAnalyticsRoomsInSpace(); + } setActiveSpaceAndCloseChat(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -108,6 +111,9 @@ void chatListHandleSpaceTap( showAlertDialog(context); } break; + case Membership.leave: + autoJoin(space); + break; default: setActiveSpaceAndCloseChat(); ErrorHandler.logError( diff --git a/lib/pangea/utils/delete_room.dart b/lib/pangea/utils/delete_room.dart deleted file mode 100644 index f8e168779..000000000 --- a/lib/pangea/utils/delete_room.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'error_handler.dart'; - -Future deleteRoom(String? roomID, Client client) async { - if (roomID == null) { - ErrorHandler.logError( - m: "in deleteRoomAction with null pangeaClassRoomID", - s: StackTrace.current, - ); - return; - } - - final Room? room = client.getRoomById(roomID); - if (room == null) { - ErrorHandler.logError( - m: "failed to fetch room with roomID $roomID", - s: StackTrace.current, - ); - return; - } - - try { - await room.join(); - } catch (err) { - ErrorHandler.logError( - m: "failed to join room with roomID $roomID", - s: StackTrace.current, - ); - return; - } - - List members; - try { - members = await room.requestParticipants(); - } catch (err) { - ErrorHandler.logError( - m: "failed to fetch members for room with roomID $roomID", - s: StackTrace.current, - ); - return; - } - - final List otherAdmins = []; - for (final User member in members) { - final String memberID = member.id; - final int memberPowerLevel = room.getPowerLevelByUserId(memberID); - if (memberID == client.userID) continue; - if (memberPowerLevel >= ClassDefaultValues.powerLevelOfAdmin) { - otherAdmins.add(member); - continue; - } - try { - await room.kick(memberID); - } catch (err) { - ErrorHandler.logError( - m: "Failed to kick user $memberID from room with id $roomID. Error: $err", - s: StackTrace.current, - ); - continue; - } - } - - if (otherAdmins.isNotEmpty && room.canSendEvent(EventTypes.RoomJoinRules)) { - try { - await client.setRoomStateWithKey( - roomID, - EventTypes.RoomJoinRules, - "", - {"join_rules": "invite"}, - ); - } catch (err) { - ErrorHandler.logError( - m: "Failed to update student create room permissions. error: $err, roomId: $roomID", - s: StackTrace.current, - ); - } - } - - try { - await room.leave(); - } catch (err) { - ErrorHandler.logError( - m: "Failed to leave room with id $roomID. Error: $err", - s: StackTrace.current, - ); - } -} diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index 6d41a9605..2fee578bf 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -43,6 +43,7 @@ class GetChatListItemSubtitle { } if (!pangeaController.languageController.languagesSet || + event.redacted || event.type != EventTypes.Message || event.messageType != MessageTypes.Text || !pangeaController.permissionsController diff --git a/lib/pangea/utils/set_class_name.dart b/lib/pangea/utils/set_class_name.dart index 92909034a..1fd2ab66d 100644 --- a/lib/pangea/utils/set_class_name.dart +++ b/lib/pangea/utils/set_class_name.dart @@ -27,6 +27,7 @@ void setClassDisplayname(BuildContext context, String? roomId) async { : L10n.of(context)!.changeTheNameOfTheChat, ), content: TextField( + maxLength: 32, controller: textFieldController, ), actions: [ diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index c516d4447..37b013b53 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -1,9 +1,13 @@ +import 'dart:developer'; + import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -41,27 +45,27 @@ class MessageSpeechToTextCardState extends State { // look for transcription in message event // if not found, call API to transcribe audio Future getSpeechToText() async { - // try { - if (l1Code == null || l2Code == null) { - throw Exception('Language selection not found'); - } - speechToTextResponse ??= - await widget.messageEvent.getSpeechToText(l1Code!, l2Code!); + try { + if (l1Code == null || l2Code == null) { + throw Exception('Language selection not found'); + } + speechToTextResponse ??= + await widget.messageEvent.getSpeechToText(l1Code!, l2Code!); - debugPrint( - 'Speech to text transcript: ${speechToTextResponse?.transcript.text}', - ); - // } catch (e, s) { - // debugger(when: kDebugMode); - // error = e; - // ErrorHandler.logError( - // e: e, - // s: s, - // data: widget.messageEvent.event.content, - // ); - // } finally { - setState(() => _fetchingTranscription = false); - // } + debugPrint( + 'Speech to text transcript: ${speechToTextResponse?.transcript.text}', + ); + } catch (e, s) { + debugger(when: kDebugMode); + error = e; + ErrorHandler.logError( + e: e, + s: s, + data: widget.messageEvent.event.content, + ); + } finally { + setState(() => _fetchingTranscription = false); + } } TextSpan _buildTranscriptText(BuildContext context) { @@ -133,6 +137,9 @@ class MessageSpeechToTextCardState extends State { getSpeechToText(); } + String? get wordsPerMinuteString => + speechToTextResponse?.transcript.wordsPerMinute?.toStringAsFixed(2); + @override Widget build(BuildContext context) { if (_fetchingTranscription) { @@ -158,11 +165,11 @@ class MessageSpeechToTextCardState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconNumberWidget( - icon: Icons.abc, - number: (selectedToken == null ? words : 1).toString(), - toolTip: L10n.of(context)!.words, - ), + // IconNumberWidget( + // icon: Icons.abc, + // number: (selectedToken == null ? words : 1).toString(), + // toolTip: L10n.of(context)!.words, + // ), IconNumberWidget( icon: Symbols.target, number: @@ -171,8 +178,9 @@ class MessageSpeechToTextCardState extends State { ), IconNumberWidget( icon: Icons.speed, - number: (selectedToken?.confidence ?? total).toString(), - toolTip: L10n.of(context)!.points, + number: + wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??", + toolTip: L10n.of(context)!.wordsPerMinute, ), ], ), diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 5c2f083d5..2f6d82ef1 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -62,6 +63,10 @@ class ToolbarDisplayController { if (controller.selectMode) { controller.clearSelectedEvents(); } + if (!MatrixState.pangeaController.languageController.languagesSet) { + pLanguageDialog(context, () {}); + return; + } focusNode.requestFocus(); final LayerLinkAndKey layerLinkAndKey = @@ -129,8 +134,11 @@ class ToolbarDisplayController { }); } - bool get highlighted => - MatrixState.pAnyState.overlay.hashCode.toString() == overlayId; + bool get highlighted { + if (overlayId == null) return false; + if (MatrixState.pAnyState.overlay == null) overlayId = null; + return MatrixState.pAnyState.overlay.hashCode.toString() == overlayId; + } } class MessageToolbar extends StatefulWidget { @@ -167,6 +175,19 @@ class MessageToolbarState extends State { debugPrint("updating toolbar mode"); final bool subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; + + if (!newMode.isValidMode(widget.pangeaMessageEvent.event)) { + ErrorHandler.logError( + e: "Invalid mode for event", + s: StackTrace.current, + data: { + "newMode": newMode, + "event": widget.pangeaMessageEvent.event, + }, + ); + return; + } + setState(() { currentMode = newMode; updatingMode = true; @@ -269,12 +290,14 @@ class MessageToolbarState extends State { PLocalKey.autoPlayMessages, ) ?? true; + + if (widget.pangeaMessageEvent.isAudioMessage) { + updateMode(MessageMode.speechToText); + return; + } + autoplay - ? updateMode( - widget.pangeaMessageEvent.isAudioMessage - ? MessageMode.speechToText - : MessageMode.textToSpeech, - ) + ? updateMode(MessageMode.textToSpeech) : updateMode(MessageMode.translation); }); @@ -345,8 +368,11 @@ class MessageToolbarState extends State { Row( mainAxisSize: MainAxisSize.min, children: MessageMode.values.map((mode) { - if ([MessageMode.definition, MessageMode.textToSpeech, MessageMode.translation] - .contains(mode) && + if ([ + MessageMode.definition, + MessageMode.textToSpeech, + MessageMode.translation, + ].contains(mode) && widget.pangeaMessageEvent.isAudioMessage) { return const SizedBox.shrink(); } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 5c7ccdef0..e8143b376 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -141,6 +141,7 @@ class OverlayMessage extends StatelessWidget { pangeaMessageEvent: pangeaMessageEvent, immersionMode: immersionMode, toolbarController: toolbarController, + isOverlay: true, ), if (event.hasAggregatedEvents( timeline, diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart index cd875cd95..eb06cd26a 100644 --- a/lib/pangea/widgets/class/add_space_toggles.dart +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -235,8 +235,13 @@ class AddToSpaceState extends State { ), activeColor: AppConfig.activeToggleColor, value: isSuggestedInSpace(possibleParent), - onChanged: (bool suggest) => - setSuggested(suggest, possibleParent), + onChanged: (bool suggest) => canAdd + ? setSuggested(suggest, possibleParent) + : ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noPermission), + ), + ), ) : Container(), ), diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 779955f82..43416e006 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -48,28 +48,33 @@ class PangeaRichTextState extends State { @override void initState() { super.initState(); - updateTextSpan(); - } - - void updateTextSpan() { - setState(() { - textSpan = getTextSpan(); - }); + setTextSpan(); } @override void didUpdateWidget(PangeaRichText oldWidget) { super.didUpdateWidget(oldWidget); - updateTextSpan(); + setTextSpan(); } - String getTextSpan() { + void _setTextSpan(String newTextSpan) { + widget.toolbarController?.toolbar?.textSelection.setMessageText( + newTextSpan, + ); + setState(() { + textSpan = newTextSpan; + }); + } + + void setTextSpan() { if (_fetchingRepresentation == true) { - return widget.pangeaMessageEvent.body; + _setTextSpan(textSpan = widget.pangeaMessageEvent.body); + return; } if (repEvent != null) { - return repEvent!.text; + _setTextSpan(repEvent!.text); + return; } if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { @@ -84,7 +89,6 @@ class PangeaRichTextState extends State { if (repEvent == null) { setState(() => _fetchingRepresentation = true); - widget.pangeaMessageEvent .representationByLanguageGlobal( langCode: widget.pangeaMessageEvent.messageDisplayLangCode, @@ -95,23 +99,17 @@ class PangeaRichTextState extends State { ) .then((event) { repEvent = event; - widget.toolbarController?.toolbar?.textSelection.setMessageText( - repEvent?.text ?? widget.pangeaMessageEvent.body, - ); + _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); }).whenComplete(() { if (mounted) { setState(() => _fetchingRepresentation = false); } }); - return widget.pangeaMessageEvent.body; - } else { - widget.toolbarController?.toolbar?.textSelection.setMessageText( - repEvent!.text, - ); - setState(() {}); - } - return repEvent!.text; + _setTextSpan(widget.pangeaMessageEvent.body); + } else { + _setTextSpan(repEvent!.text); + } } @override @@ -190,7 +188,10 @@ class PangeaRichTextState extends State { return blur > 0 ? ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), + imageFilter: ImageFilter.blur( + sigmaX: blur, + sigmaY: blur, + ), child: richText, ) : richText; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index a6ef7b511..4b8445417 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -314,6 +314,23 @@ class PartOfSpeechBlock extends StatelessWidget { required this.languageType, }); + String get exampleSentence => languageType == LanguageType.target + ? wordData.targetExampleSentence + : wordData.baseExampleSentence; + + String get definition => languageType == LanguageType.target + ? wordData.targetDefinition + : wordData.baseDefinition; + + String formattedTitle(BuildContext context) { + final String word = languageType == LanguageType.target + ? wordData.targetWord + : wordData.baseWord; + String? pos = wordData.formattedPartOfSpeech(languageType); + if (pos == null || pos.isEmpty) pos = L10n.of(context)!.unkDisplayName; + return "$word (${wordData.formattedPartOfSpeech(languageType)})"; + } + @override Widget build(BuildContext context) { return Padding( @@ -324,9 +341,7 @@ class PartOfSpeechBlock extends StatelessWidget { Align( alignment: Alignment.centerLeft, child: Text( - languageType == LanguageType.target - ? "${wordData.targetWord} (${wordData.formattedPartOfSpeech(languageType)})" - : "${wordData.baseWord} (${wordData.formattedPartOfSpeech(languageType)})", + formattedTitle(context), style: BotStyle.text(context, italics: true, bold: false), ), ), @@ -337,47 +352,43 @@ class PartOfSpeechBlock extends StatelessWidget { alignment: Alignment.centerLeft, child: Column( children: [ - RichText( - text: TextSpan( - style: BotStyle.text( - context, - italics: false, - bold: false, + if (definition.isNotEmpty) + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, + ), + children: [ + TextSpan( + text: "${L10n.of(context)!.definition}: ", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: definition), + ], ), - children: [ - TextSpan( - text: "${L10n.of(context)!.definition}: ", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: languageType == LanguageType.target - ? wordData.targetDefinition - : wordData.baseDefinition, - ), - ], ), - ), const SizedBox(height: 10), - RichText( - text: TextSpan( - style: BotStyle.text( - context, - italics: false, - bold: false, + if (exampleSentence.isNotEmpty) + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, + ), + children: [ + TextSpan( + text: "${L10n.of(context)!.exampleSentence}: ", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: exampleSentence), + ], ), - children: [ - TextSpan( - text: "${L10n.of(context)!.exampleSentence}: ", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: languageType == LanguageType.target - ? wordData.targetExampleSentence - : wordData.baseExampleSentence, - ), - ], ), - ), ], ), ), diff --git a/lib/pangea/widgets/user_settings/language_tile.dart b/lib/pangea/widgets/user_settings/language_tile.dart index 085779781..d66335300 100644 --- a/lib/pangea/widgets/user_settings/language_tile.dart +++ b/lib/pangea/widgets/user_settings/language_tile.dart @@ -1,19 +1,19 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import '../flag.dart'; -import 'p_language_dialog.dart'; //PTODO - move this to settings_learning_view.dart and make callback a setState class LanguageTile extends StatelessWidget { + final SettingsLearningController learningController; final PangeaController pangeaController = MatrixState.pangeaController; - LanguageTile({super.key}); + LanguageTile(this.learningController, {super.key}); @override Widget build(BuildContext context) { @@ -81,7 +81,9 @@ class LanguageTile extends StatelessWidget { ], ), trailing: const Icon(Icons.edit_outlined), - onTap: () => pLanguageDialog(context, () {}), + onTap: () async { + learningController.changeLanguage(); + }, ); } } diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index 077959ead..4d09b506e 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -15,7 +15,7 @@ import '../../../widgets/matrix.dart'; import 'p_language_dropdown.dart'; import 'p_question_container.dart'; -pLanguageDialog(BuildContext parentContext, Function callback) { +pLanguageDialog(BuildContext parentContext, Function callback) async { final PangeaController pangeaController = MatrixState.pangeaController; //PTODO: if source language not set by user, default to languge from device settings final LanguageModel? userL1 = pangeaController.languageController.userL1; diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 6c1078167..781b8ab61 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -55,7 +55,7 @@ class ChatSettingsPopupMenuState extends State { value: 'learning_settings', child: Row( children: [ - const Icon(Icons.settings), + const Icon(Icons.psychology_outlined), const SizedBox(width: 12), Text(L10n.of(context)!.learningSettings), ], @@ -83,19 +83,22 @@ class ChatSettingsPopupMenuState extends State { ], ), ), - PopupMenuItem( - value: 'leave', - child: Row( - children: [ - // #Pangea - // const Icon(Icons.delete_outlined), - const Icon(Icons.arrow_forward), - // Pangea# - const SizedBox(width: 12), - Text(L10n.of(context)!.leave), - ], + // #Pangea + if (!widget.room.isArchived) + // Pangea# + PopupMenuItem( + value: 'leave', + child: Row( + children: [ + // #Pangea + // const Icon(Icons.delete_outlined), + const Icon(Icons.arrow_forward), + // Pangea# + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], + ), ), - ), // #Pangea if (classSettings != null) PopupMenuItem( diff --git a/needed-translations.txt b/needed-translations.txt index 60c1be8f4..af36b97ba 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -821,7 +821,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -836,7 +835,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "be": [ @@ -2256,7 +2259,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -2271,7 +2273,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "bn": [ @@ -3153,7 +3159,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -3168,7 +3173,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "bo": [ @@ -4050,7 +4059,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -4065,7 +4073,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ca": [ @@ -4947,7 +4959,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -4962,7 +4973,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "cs": [ @@ -5844,7 +5859,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -5859,7 +5873,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "de": [ @@ -6688,7 +6706,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -6703,7 +6720,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "el": [ @@ -7585,7 +7606,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -7600,7 +7620,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "eo": [ @@ -8482,7 +8506,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -8497,37 +8520,14 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "es": [ - "presenceStyle", - "presencesToggle", - "writeAMessageFlag", - "youInvitedToBy", - "hidePresences", - "sendReadReceipts", - "sendTypingNotificationsDescription", - "sendReadReceiptsDescription", - "formattedMessages", - "formattedMessagesDescription", - "verifyOtherUser", - "verifyOtherUserDescription", - "verifyOtherDevice", - "verifyOtherDeviceDescription", - "transparent", - "incomingMessages", - "stickers", - "commandHint_ignore", - "commandHint_unignore", - "unreadChatsInApp", - "messageAnalytics", - "words", - "score", - "accuracy", - "points", - "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -8542,7 +8542,9 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing" ], "et": [ @@ -9367,7 +9369,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -9382,7 +9383,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "eu": [ @@ -10207,7 +10212,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -10222,7 +10226,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "fa": [ @@ -11104,7 +11112,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -11119,7 +11126,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "fi": [ @@ -12001,7 +12012,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -12016,7 +12026,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "fr": [ @@ -12898,7 +12912,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -12913,7 +12926,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ga": [ @@ -13795,7 +13812,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -13810,7 +13826,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "gl": [ @@ -14635,7 +14655,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -14650,7 +14669,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "he": [ @@ -15532,7 +15555,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -15547,7 +15569,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "hi": [ @@ -16429,7 +16455,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -16444,7 +16469,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "hr": [ @@ -17313,7 +17342,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -17328,7 +17356,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "hu": [ @@ -18210,7 +18242,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -18225,7 +18256,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ia": [ @@ -19631,7 +19666,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -19646,7 +19680,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "id": [ @@ -20528,7 +20566,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -20543,7 +20580,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ie": [ @@ -21425,7 +21466,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -21440,7 +21480,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "it": [ @@ -22307,7 +22351,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -22322,7 +22365,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ja": [ @@ -23204,7 +23251,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -23219,7 +23265,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ko": [ @@ -24101,7 +24151,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -24116,7 +24165,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "lt": [ @@ -24998,7 +25051,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -25013,7 +25065,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "lv": [ @@ -25895,7 +25951,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -25910,7 +25965,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "nb": [ @@ -26792,7 +26851,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -26807,7 +26865,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "nl": [ @@ -27689,7 +27751,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -27704,7 +27765,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "pl": [ @@ -28586,7 +28651,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -28601,7 +28665,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "pt": [ @@ -29483,7 +29551,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -29498,7 +29565,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "pt_BR": [ @@ -30349,7 +30420,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -30364,7 +30434,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "pt_PT": [ @@ -31246,7 +31320,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -31261,7 +31334,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ro": [ @@ -32143,7 +32220,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -32158,7 +32234,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ru": [ @@ -32983,7 +33063,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -32998,7 +33077,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "sk": [ @@ -33880,7 +33963,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -33895,7 +33977,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "sl": [ @@ -34777,7 +34863,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -34792,7 +34877,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "sr": [ @@ -35674,7 +35763,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -35689,7 +35777,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "sv": [ @@ -36536,7 +36628,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -36551,7 +36642,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "ta": [ @@ -37433,7 +37528,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -37448,7 +37542,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "th": [ @@ -38330,7 +38428,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -38345,7 +38442,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "tr": [ @@ -39212,7 +39313,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -39227,7 +39327,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "uk": [ @@ -40052,7 +40156,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -40067,7 +40170,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "vi": [ @@ -40949,7 +41056,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -40964,7 +41070,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "zh": [ @@ -41789,7 +41899,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -41804,7 +41913,11 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ], "zh_Hant": [ @@ -42686,7 +42799,6 @@ "accuracy", "points", "noPaymentInfo", - "updatePhoneOS", "conversationBotModeSelectDescription", "conversationBotModeSelectOption_discussion", "conversationBotModeSelectOption_custom", @@ -42701,6 +42813,10 @@ "conversationBotDiscussionZone_discussionTriggerScheduleEnabledLabel", "conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel", "conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel", - "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel" + "conversationBotDiscussionZone_discussionTriggerReactionKeyLabel", + "studentAnalyticsNotAvailable", + "roomDataMissing", + "updatePhoneOS", + "wordsPerMinute" ] } diff --git a/pubspec.lock b/pubspec.lock index 54e1ac6a7..8932ea070 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1841,50 +1841,58 @@ packages: dependency: "direct main" description: name: record - sha256: f703397f5a60d9b2b655b3acc94ba079b2d9a67dc0725bdb90ef2fee2441ebf7 + sha256: "113b368168c49c78902ab37c2b354dea30a0aec5bdeca434073826b6ea73eca1" url: "https://pub.dev" source: hosted - version: "4.4.4" + version: "5.0.5" + record_android: + dependency: transitive + description: + name: record_android + sha256: "0df98e05873b22b443309e289bf1eb3b5b9a60e7779134334e2073eb0763a992" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + record_darwin: + dependency: transitive + description: + name: record_darwin + sha256: ee8cb1bb1712d7ce38140ecabe70e5c286c02f05296d66043bee865ace7eb1b9 + url: "https://pub.dev" + source: hosted + version: "1.0.1" record_linux: dependency: transitive description: name: record_linux - sha256: "348db92c4ec1b67b1b85d791381c8c99d7c6908de141e7c9edc20dad399b15ce" + sha256: "7d0e70cd51635128fe9d37d89bafd6011d7cbba9af8dc323079ae60f23546aef" url: "https://pub.dev" source: hosted - version: "0.4.1" - record_macos: - dependency: transitive - description: - name: record_macos - sha256: d1d0199d1395f05e218207e8cacd03eb9dc9e256ddfe2cfcbbb90e8edea06057 - url: "https://pub.dev" - source: hosted - version: "0.2.2" + version: "0.7.1" record_platform_interface: dependency: transitive description: name: record_platform_interface - sha256: "7a2d4ce7ac3752505157e416e4e0d666a54b1d5d8601701b7e7e5e30bec181b4" + sha256: "3a4b56e94ecd2a0b2b43eb1fa6f94c5b8484334f5d38ef43959c4bf97fb374cf" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "1.0.2" record_web: dependency: transitive description: name: record_web - sha256: "219ffb4ca59b4338117857db56d3ffadbde3169bcaf1136f5f4d4656f4a2372d" + sha256: "24847cdbcf999f7a5762170792f622ac844858766becd0f2370ec8ae22f7526e" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "1.0.5" record_windows: dependency: transitive description: name: record_windows - sha256: "42d545155a26b20d74f5107648dbb3382dbbc84dc3f1adc767040359e57a1345" + sha256: "39998b3ea7d8d28b04159d82220e6e5e32a7c357c6fb2794f5736beea272f6c3" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "1.0.2" remove_emoji: dependency: transitive description: @@ -1945,18 +1953,18 @@ packages: dependency: transitive description: name: sentry - sha256: e572d33a3ff1d69549f33ee828a8ff514047d43ca8eea4ab093d72461205aa3e + sha256: fd1fbfe860c05f5c52820ec4dbf2b6473789e83ead26cfc18bca4fe80bf3f008 url: "https://pub.dev" source: hosted - version: "7.20.1" + version: "8.2.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: ac8cf6bb849f3560353ae33672e17b2713809a4e8de0d3cf372e9e9c42013757 + sha256: c64f0aec5332bec87083b61514d1b6b29e435b9045d03ce1575861192b9a5680 url: "https://pub.dev" source: hosted - version: "7.20.1" + version: "8.2.0" share_plus: dependency: "direct main" description: