merge conflicts

This commit is contained in:
ggurdin 2024-05-23 09:14:21 -04:00
commit 0454e1419d
75 changed files with 1639 additions and 1385 deletions

View file

@ -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 <DEVICE_NAME>`
![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)
<a href='https://ko-fi.com/C1C86VN53' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi5.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
### 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)
* <a href="https://github.com/fabiyamada">Fabiyamada</a> is a graphics designer and has made the fluffychat logo and the banner. Big thanks for her great designs.
* <a href="https://github.com/advocatux">Advocatux</a> has made the Spanish translation with great love and care. He always stands by my side and supports my work with great commitment.

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Archive> {
Future<List<Room>> 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 {

View file

@ -481,7 +481,16 @@ class ChatController extends State<ChatPageWithRoom>
}
// 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<ChatPageWithRoom>
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<ChatPageWithRoom>
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<ChatPageWithRoom>
bool get canEditSelectedEvents {
if (isArchived ||
selectedEvents.length != 1 ||
// #Pangea
selectedEvents.single.messageType != MessageTypes.Text ||
// Pangea#
!selectedEvents.first.status.isSent) {
return false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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: <Widget>[
// #Pangeas
// #Pangea
//if (room.canonicalAlias.isNotEmpty)
// IconButton(
// tooltip: L10n.of(context)!.share,

View file

@ -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<ChatList>
_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<ChatList>
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<ChatList>
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<ChatList>
pangeaController.afterSyncAndFirstLoginInitialization(context);
await pangeaController.inviteBotToExistingSpaces();
await pangeaController.setPangeaPushRules();
await client.migrateAnalyticsRooms();
} else {
ErrorHandler.logError(
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",

View file

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

View file

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

View file

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

View file

@ -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<SpaceView> {
// #Pangea
StreamSubscription<SyncUpdate>? _roomSubscription;
bool refreshing = false;
final String _chatCountsKey = 'chatCounts';
Map<String, int> 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<SpaceView> {
// Pangea#
}
Future<GetSpaceHierarchyResponse> loadHierarchy([String? prevBatch]) async {
Future<GetSpaceHierarchyResponse> 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<SpaceView> {
});
// 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<SpaceView> {
});
rethrow;
} finally {
// #Pangea
if (activeSpace != null) {
await setChatCount(
activeSpace,
_lastResponse[activeSpaceId],
);
}
// Pangea#
setState(() {
loading = false;
});
@ -173,7 +204,7 @@ class _SpaceViewState extends State<SpaceView> {
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<SpaceView> {
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<SpaceView> {
// #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<SpaceView> {
}
// #Pangea
Future<void> 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<void> 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<SpaceView> {
}
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<SpaceRoomsChunk> filterSpaceChildren(
Room space,
List<SpaceRoomsChunk> 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<void> setChatCount(
Room space,
GetSpaceHierarchyResponse? response,
) async {
final Map<String, int> updatedChatCounts = Map.from(chatCounts);
final List<SpaceRoomsChunk> 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<SpaceView> {
// #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<SpaceView> {
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<SpaceView> {
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<SpaceView> {
),
// #Pangea
// onTap: () => _onJoinSpaceChild(spaceChild),
onTap: () => context.push(
'/spaces/${spaceChild.roomId}',
onTap: () => context.go(
'/rooms/${spaceChild.roomId}/details',
),
// Pangea#
),

View file

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

View file

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

View file

@ -157,7 +157,6 @@ class InvitationSelectionController extends State<InvitationSelection> {
//#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<InvitationSelection> {
// #Pangea
Future<void> 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;

View file

@ -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<NewGroup> {
void setVocab(List<Lemma> 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);

View file

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

View file

@ -216,7 +216,7 @@ class NewSpaceController extends State<NewSpace> {
);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
context.push('/spaces/$spaceId');
context.go("/rooms/$spaceId/details");
return;
}
@ -245,7 +245,7 @@ class NewSpaceController extends State<NewSpace> {
// context.pop<String>(spaceId);
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
context.push('/spaces/$spaceId');
context.go("/rooms/$spaceId/details");
// Pangea#
} catch (e) {
setState(() {

View file

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

View file

@ -42,6 +42,9 @@ class SettingsController extends State<Settings> {
cancelLabel: L10n.of(context)!.cancel,
textFields: [
DialogTextField(
// #Pangea
maxLength: 32,
// Pangea#
initialText: profile?.displayName ??
Matrix.of(context).client.userID!.localpart,
),

View file

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

View file

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

View file

@ -211,7 +211,8 @@ class Choreographer {
final CanSendStatus canSendStatus =
pangeaController.subscriptionController.canSendStatus;
if (canSendStatus != CanSendStatus.subscribed) {
if (canSendStatus != CanSendStatus.subscribed ||
(!igcEnabled && !itEnabled)) {
return;
}

View file

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

View file

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

View file

@ -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<String, dynamic> 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<Map<String, dynamic>> detections;
String fullText;
LanguageDetectionResponse({
required this.detections,
required this.fullText,
});
factory LanguageDetectionResponse.fromJson(Map<String, dynamic> json) {
return LanguageDetectionResponse(
detections: List<Map<String, dynamic>>.from(json['detections']),
fullText: json['full_text'],
);
}
Map<String, dynamic> toJson() {
return {
'detections': detections,
'full_text': fullText,
};
}
}
class _LanguageDetectionCacheItem {
Future<LanguageDetectionResponse> data;
_LanguageDetectionCacheItem({
required this.data,
});
}
class LanguageDetectionController {
static final Map<LanguageDetectionRequest, _LanguageDetectionCacheItem>
_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<LanguageDetectionResponse> get(
LanguageDetectionRequest params,
) async {
if (_cache.containsKey(params)) {
return _cache[params]!.data;
} else {
final Future<LanguageDetectionResponse> response = _fetchResponse(
await _pangeaController.userController.accessToken,
params,
);
_cache[params] = _LanguageDetectionCacheItem(data: response);
return response;
}
}
static Future<LanguageDetectionResponse> _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<String, dynamic> json = jsonDecode(res.body);
return LanguageDetectionResponse.fromJson(json);
}
}

View file

@ -90,7 +90,7 @@ class MyAnalyticsController {
}
final Room analyticsRoom = await _pangeaController.matrixState.client
.getMyAnalyticsRoom(langCode);
analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom();
final List<Future<void>> saveFutures = [];
for (final uses in aggregatedVocabUse.entries) {
debugPrint("saving of type ${uses.value.first.constructType}");

View file

@ -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<void> setPangeaPushRules() async {
final List<Room> 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,
) ??

View file

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

View file

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

View file

@ -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<Room> 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<void> updateAnalyticsRoomVisibility() async {
final List<Future> 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<void> addAnalyticsRoomsToAllSpaces() async {
final List<Future> 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<void> inviteAllTeachersToAllAnalyticsRooms() async {
final List<Future> 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<void> joinAnalyticsRoomsInAllSpaces() async {
final List<Future> 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<void> 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<void> migrateAnalyticsRooms() async {
await updateAnalyticsRoomVisibility();
await addAnalyticsRoomsToAllSpaces();
await inviteAllTeachersToAllAnalyticsRooms();
await joinInvitedAnalyticsRooms();
await joinAnalyticsRoomsInAllSpaces();
}
}

View file

@ -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<Event?> _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<void> 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<String> 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<void> addAnalyticsRoomToSpace(Room analyticsRoom) async {
if (!isSpace) {
debugPrint("addAnalyticsRoomToSpace called on non-space room");
Sentry.addBreadcrumb(
Breadcrumb(
message: "addAnalyticsRoomToSpace called on non-space room",
),
);
return Future.value();
}
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<void> 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<void> addAnalyticsRoomsToSpace() async {
await postLoad();
final List<Room> allMyAnalyticsRooms = client.allMyAnalyticsRooms;
for (final Room analyticsRoom in allMyAnalyticsRooms) {
await addAnalyticsRoomToSpace(analyticsRoom);
}
}
// invite teachers of 1 space to 1 analytics room
Future<void> 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<User> 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<void> 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<void> 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<void> 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<String> 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<void> 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<void> ensureAnalyticsRoomExists() async {
await postLoad();
if (firstLanguageSettings?.targetLanguage == null) return;
await client.getMyAnalyticsRoom(firstLanguageSettings!.targetLanguage);
}
}

View file

@ -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<PangeaMatch>? 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<PangeaMatch> steps = repEvent!.choreo!.choreoSteps
.where(
(choreoStep) =>
choreoStep.acceptedOrIgnoredMatch != null &&
choreoStep.acceptedOrIgnoredMatch?.match.shortMessage == lemma,
)
?.acceptedOrIgnoredMatch;
return step;
.map((element) => element.acceptedOrIgnoredMatch)
.cast<PangeaMatch>()
.toList();
return steps;
}
// List<SpanData> get activities =>

View file

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

View file

@ -60,7 +60,7 @@ class PangeaToken {
static const String _lemmaKey = ModelKey.lemma;
Map<String, dynamic> toJson() => {
_textKey: text,
_textKey: text.toJson(),
_hasInfoKey: hasInfo,
_lemmaKey: lemmas.map((e) => e.toJson()).toList(),
};

View file

@ -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<String, dynamic> json) {
@ -117,7 +117,7 @@ class STTToken {
}
Map<String, dynamic> 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<STTToken> 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<String, dynamic> 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<String, dynamic> 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 {

View file

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

View file

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

View file

@ -52,7 +52,11 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
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<AnalyticsListTile> {
: 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

View file

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

View file

@ -246,6 +246,15 @@ class BaseAnalyticsView extends StatelessWidget {
.widget
.tabs[1]
.allowNavigateOnSelect,
enabled:
controller.enableSelection(
AnalyticsSelected(
item.id,
controller
.widget.tabs[1].type,
"",
),
),
),
)
.toList(),

View file

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

View file

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

View file

@ -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<ConstructList> {
selected: widget.selected,
forceUpdate: true,
)
.then((_) => setState(() => initialized = true));
.whenComplete(() => setState(() => initialized = true));
}
@override
@ -160,11 +161,11 @@ class ConstructListViewState extends State<ConstructListView> {
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<ConstructListView> {
}
setState(() => fetchingUses = true);
final List<OneConstructUse> uses = currentConstruct!.content.uses;
_msgEvents.clear();
try {
final List<OneConstructUse> 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<ConstructEvent>? get constructs =>
@ -237,6 +248,38 @@ class ConstructListViewState extends State<ConstructListView> {
(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<MessageEventMatch> getMessageEventMatches() {
if (widget.controller.currentLemma == null) return [];
final List<MessageEventMatch> 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<PangeaMatch>? 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<ConstructListView> {
);
}
final msgEventMatches = getMessageEventMatches();
return widget.controller.currentLemma == null
? Expanded(
child: ListView.builder(
@ -278,23 +323,22 @@ class ConstructListViewState extends State<ConstructListView> {
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<ConstructListView> {
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,
});
}

View file

@ -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<ClassSettingsPage> createState() => ClassSettingsController();
}
class ClassSettingsController extends State<ClassSettingsPage> {
PangeaController pangeaController = MatrixState.pangeaController;
final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
final GlobalKey<ClassSettingsState> classSettingsKey =
GlobalKey<ClassSettingsState>();
Room? room;
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
Future<void> 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();
}

View file

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

View file

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

View file

@ -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<void> deleteSpace() async {
final Client client = Matrix.of(context).client;
final GetSpaceHierarchyResponse spaceHierarchy =
await client.getSpaceHierarchy(room.id);
if (spaceHierarchy.rooms.isNotEmpty) {
final List<Room> 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<Room>()
.toList();
await Future.wait(
spaceChats.map((c) => deleteRoom(c.id, client)),
);
}
deleteRoom(room.id, client);
context.go('/rooms');
return;
}
Future<void> deleteChat() {
context.go('/rooms');
return deleteRoom(room.id, Matrix.of(context).client);
}
Future<void> 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: <Widget>[
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(),
);
}
}

View file

@ -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<NewClass> {
TextEditingController controller = TextEditingController();
final PangeaController pangeaController = MatrixState.pangeaController;
final GlobalKey<RoomRulesState> rulesEditorKey = GlobalKey<RoomRulesState>();
final GlobalKey<ClassSettingsState> classSettingsKey =
GlobalKey<ClassSettingsState>();
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);
}

View file

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

View file

@ -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<SettingsLearning> {
});
}
Future<void> changeLanguage() async {
await pLanguageDialog(context, () {});
setState(() {});
}
@override
void dispose() {
super.dispose();

View file

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

View file

@ -1,55 +0,0 @@
import '../config/environment.dart';
import '../network/requests.dart';
import '../network/urls.dart';
class MessageServiceRepo {
static Future<void> 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<int> 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,
};
}
}

View file

@ -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<void> pangeaAddToSpace(
Room space,
List<String> 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<void> pangeaAddToSpace(
if (room != null && chatIsInSpace(room, space)) {
throw L10n.of(context)!.alreadyInSpace;
}
await space.setSpaceChild(roomId);
await space.setSpaceChild(roomId, suggested: suggested);
}
}

View file

@ -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<void> archiveSpace(Room? space, Client client) async {
if (space == null) {
@ -14,6 +13,9 @@ Future<void> archiveSpace(Room? space, Client client) async {
final List<Room> children = await space.getChildRooms();
for (final Room child in children) {
if (child.isUnread) {
await child.markUnread(false);
}
await child.leave();
}
await space.leave();

View file

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

View file

@ -1,89 +0,0 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'error_handler.dart';
Future<void> 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<User> 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<User> 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,
);
}
}

View file

@ -43,6 +43,7 @@ class GetChatListItemSubtitle {
}
if (!pangeaController.languageController.languagesSet ||
event.redacted ||
event.type != EventTypes.Message ||
event.messageType != MessageTypes.Text ||
!pangeaController.permissionsController

View file

@ -27,6 +27,7 @@ void setClassDisplayname(BuildContext context, String? roomId) async {
: L10n.of(context)!.changeTheNameOfTheChat,
),
content: TextField(
maxLength: 32,
controller: textFieldController,
),
actions: [

View file

@ -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<MessageSpeechToTextCard> {
// look for transcription in message event
// if not found, call API to transcribe audio
Future<void> 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<MessageSpeechToTextCard> {
getSpeechToText();
}
String? get wordsPerMinuteString =>
speechToTextResponse?.transcript.wordsPerMinute?.toStringAsFixed(2);
@override
Widget build(BuildContext context) {
if (_fetchingTranscription) {
@ -158,11 +165,11 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
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<MessageSpeechToTextCard> {
),
IconNumberWidget(
icon: Icons.speed,
number: (selectedToken?.confidence ?? total).toString(),
toolTip: L10n.of(context)!.points,
number:
wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??",
toolTip: L10n.of(context)!.wordsPerMinute,
),
],
),

View file

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

View file

@ -141,6 +141,7 @@ class OverlayMessage extends StatelessWidget {
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
toolbarController: toolbarController,
isOverlay: true,
),
if (event.hasAggregatedEvents(
timeline,

View file

@ -235,8 +235,13 @@ class AddToSpaceState extends State<AddToSpaceToggles> {
),
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(),
),

View file

@ -48,28 +48,33 @@ class PangeaRichTextState extends State<PangeaRichText> {
@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<PangeaRichText> {
if (repEvent == null) {
setState(() => _fetchingRepresentation = true);
widget.pangeaMessageEvent
.representationByLanguageGlobal(
langCode: widget.pangeaMessageEvent.messageDisplayLangCode,
@ -95,23 +99,17 @@ class PangeaRichTextState extends State<PangeaRichText> {
)
.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<PangeaRichText> {
return blur > 0
? ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
imageFilter: ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
),
child: richText,
)
: richText;

View file

@ -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>[
TextSpan(
text: "${L10n.of(context)!.definition}: ",
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: definition),
],
),
children: <TextSpan>[
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>[
TextSpan(
text: "${L10n.of(context)!.exampleSentence}: ",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: exampleSentence),
],
),
children: <TextSpan>[
TextSpan(
text: "${L10n.of(context)!.exampleSentence}: ",
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: languageType == LanguageType.target
? wordData.targetExampleSentence
: wordData.baseExampleSentence,
),
],
),
),
],
),
),

View file

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

View file

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

View file

@ -55,7 +55,7 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
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<ChatSettingsPopupMenu> {
],
),
),
PopupMenuItem<String>(
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<String>(
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<String>(

View file

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

View file

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